├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── README.md ├── docs.md └── src ├── .gitattributes ├── config ├── args.rs ├── mod.rs ├── structs.rs └── utils.rs ├── diff.rs ├── lib.rs ├── main.rs ├── network ├── mod.rs ├── request.rs ├── response.rs ├── tests.rs └── utils.rs ├── runner ├── logic.rs ├── mod.rs ├── output.rs ├── runner.rs └── utils.rs └── utils.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /tests 3 | *workspace* 4 | .vscode 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "x8" 3 | version = "4.3.1-main" 4 | authors = ["Alexander Mironov "] 5 | edition = "2018" 6 | license = "GPL-3.0-or-later" 7 | homepage = "https://github.com/Sh1Yo/x8" 8 | repository = "https://github.com/Sh1Yo/x8" 9 | description = "Hidden parameters discovery suite." 10 | categories = ["command-line-utilities"] 11 | keywords = ["security", "web", "recon", "content-discovery"] 12 | readme = "README.md" 13 | 14 | [dependencies] 15 | tokio = { version = "1", features = ["full"] } 16 | futures = "0.3.15" 17 | url = { git = "https://github.com/raw-http/rust-url"} 18 | http = { git = "https://github.com/raw-http/http"} 19 | reqwest = { git = "https://github.com/raw-http/reqwest", features = ["socks", "cookies", "json", "rustls-tls", "trust-dns", "gzip"] } 20 | regex = "1.3.7" 21 | percent-encoding = "2.1.0" 22 | lazy_static = "1.4.0" 23 | clap = "2.33.1" 24 | rand = "0.5.0" 25 | itertools = "0.8.2" 26 | env_logger = "0.7.1" 27 | colored = "2" 28 | diffs = "0.2.1" 29 | parking_lot = "0.11" 30 | log = "0.4.14" 31 | atty = "0.2" 32 | async-recursion = "1.0.0" 33 | serde = "1.0" 34 | serde_json = "1.0" 35 | indicatif = "0.17.1" 36 | linked-hash-map = "0.5.6" 37 | strip-ansi-escapes = "0.1.1" 38 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:alpine as builder 2 | 3 | RUN apk add --no-cache build-base openssl-dev 4 | 5 | WORKDIR /app/x8 6 | COPY Cargo.toml Cargo.lock ./ 7 | COPY src/ src/ 8 | 9 | RUN cargo build --release 10 | 11 | FROM alpine:3.12 12 | COPY --from=builder /app/x8/target/release/x8 /usr/local/bin/x8 13 | ENTRYPOINT [ "x8" ] 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Twitter](https://img.shields.io/twitter/follow/sh1yo_.svg?logo=twitter)](https://twitter.com/sh1yo_) 2 | ![stars](https://img.shields.io/github/stars/Sh1Yo/x8) 3 | [![issues](https://img.shields.io/github/issues/sh1yo/x8?color=%20%237fb3d5%20)](https://github.com/sh1yo/x8/issues) 4 | 5 | [![Latest Version](https://img.shields.io/github/release/sh1yo/x8.svg?style=flat-square)](https://github.com/sh1yo/x8/releases) 6 | [![crates.io](https://img.shields.io/crates/v/x8.svg)](https://crates.io/crates/x8) 7 | [![crates_downloads](https://img.shields.io/crates/d/x8?logo=rust)](https://crates.io/crates/x8) 8 | [![github_downloads](https://img.shields.io/github/downloads/sh1yo/x8/total?label=downloads&logo=github)](https://github.com/sh1yo/x8/releases) 9 | 10 | 11 | 12 |

x8

13 | 14 |

Hidden parameters discovery suite written in Rust.

15 | 16 |

17 | 18 | The tool aids in identifying hidden parameters that could potentially be vulnerable or reveal interesting functionality that may be missed by other testers. Its high accuracy is achieved through line-by-line comparison of pages, comparison of response codes, and reflections. 19 | 20 | # Documentation 21 | 22 | The documentation that explains every feature can be accessed at [https://sh1yo.art/x8docs/](https://sh1yo.art/x8docs/). The source of the documentation is located at [/docs.md](docs.md). 23 | 24 | # Tree 25 | 26 | - [Features](#features) 27 | - [Examples](#examples) 28 | - [Test site](#test-site) 29 | - [Usage](#usage) 30 | - [Wordlists](#wordlists) 31 | - [Burp Suite integration](#burp-suite-integration) 32 | - [Installation](#installation) 33 | 34 | # Features 35 | 36 | - Fast. 37 | - Offers flexible request configuration through the use of templates and injection points. 38 | - Highly scalable, capable of checking thousands of URLs per run. 39 | - Provides higher accuracy compared to similar tools, especially in difficult cases. 40 | - Capable of discovering parameters with non-random values, such as admin=true. 41 | - Highly configurable with a wide range of customizable options. 42 | - Achieves almost raw requests through external library modification. 43 | 44 | # Examples 45 | 46 | #### Check parameters in query 47 | 48 | ```bash 49 | x8 -u "https://example.com/" -w 50 | ``` 51 | 52 | With default parameters: 53 | ```bash 54 | x8 -u "https://example.com/?something=1" -w 55 | ``` 56 | 57 | `/?something=1` equals to `/?something=1&%s` 58 | 59 | #### Send parameters via body 60 | 61 | ```bash 62 | x8 -u "https://example.com/" -X POST -w 63 | ``` 64 | 65 | Or with a custom body: 66 | ```bash 67 | x8 -u "https://example.com/" -X POST -b '{"x":{%s}}' -w 68 | ``` 69 | `%s` will be replaced with different parameters like `{"x":{"a":"b3a1a", "b":"ce03a", ...}}` 70 | 71 | #### Check multiple urls in paralell 72 | 73 | ```bash 74 | x8 -u "https://example.com/" "https://4rt.one/" -W0 75 | ``` 76 | 77 | #### Custom template 78 | 79 | ```bash 80 | x8 -u "https://example.com/" --param-template "user[%k]=%v" -w 81 | ``` 82 | 83 | Now every request would look like `/?user[a]=hg2s4&user[b]=a34fa&...` 84 | 85 | #### Percent encoding 86 | 87 | Sometimes parameters should be encoded. It is also possible: 88 | 89 | ```bash 90 | x8 -u "https://example.com/?path=..%2faction.php%3f%s%23" --encode -w 91 | ``` 92 | 93 | ```http 94 | GET /?path=..%2faction.php%3fWTDa8%3Da7UOS%26rTIDA%3DexMFp...%23 HTTP/1.1 95 | Host: example.com 96 | ``` 97 | 98 | #### Search for headers 99 | 100 | ```bash 101 | x8 -u "https://example.com" --headers -w 102 | ``` 103 | 104 | #### Search for header values 105 | 106 | You can also target single headers: 107 | 108 | ```bash 109 | x8 -u "https://example.com" --headers -H "Cookie: %s" -w 110 | ``` 111 | 112 | # Test site 113 | 114 | You can check the tool and compare it with other tools on the following urls: 115 | 116 | `https://4rt.one/level1` (GET) 117 | 118 | `https://4rt.one/level3` (GET) 119 | 120 | # Usage 121 | 122 | ``` 123 | USAGE: 124 | x8 [FLAGS] [OPTIONS] 125 | 126 | FLAGS: 127 | --append Append to the output file instead of overwriting it. 128 | -B Equal to -x http://localhost:8080 129 | --check-binary Check the body of responses with binary content types 130 | --disable-additional-checks Private 131 | --disable-colors 132 | --disable-custom-parameters Do not automatically check parameters like admin=true 133 | --disable-progress-bar 134 | --disable-trustdns Can solve some dns related problems 135 | --encode Encodes query or body before making a request, i.e & -> %26, = -> %3D 136 | List of chars to encode: ", `, , <, >, &, #, ;, /, =, % 137 | -L, --follow-redirects Follow redirections 138 | --force Force searching for parameters on pages > 25MB. Remove an error in case there's 1 139 | worker with --one-worker-per-host option. 140 | -h, --help Prints help information 141 | --headers Switch to header discovery mode. 142 | NOTE Content-Length and Host headers are automatically removed from the list 143 | --invert By default, parameters are sent within the body only in case PUT or POST methods 144 | are used. 145 | It's possible to overwrite this behavior by specifying the option 146 | --mimic-browser Add default headers that browsers usually set. 147 | --one-worker-per-host Multiple urls with the same host will be checked one after another, 148 | while urls with different hosts - are in parallel. 149 | Doesn't increase the number of workers 150 | --reflected-only Disable page comparison and search for reflected parameters only. 151 | --remove-empty Skip writing to file outputs of url:method pairs without found parameters 152 | --replay-once If a replay proxy is specified, send all found parameters within one request. 153 | --strict Only report parameters that have changed the different parts of a page 154 | --test Prints request and response 155 | -V, --version Prints version information 156 | --verify Verify found parameters. 157 | 158 | OPTIONS: 159 | -b, --body Example: --body '{"x":{%s}}' 160 | Available variables: {{random}} 161 | -c The number of concurrent requests per url [default: 1] 162 | --custom-parameters 163 | Check these parameters with non-random values like true/false yes/no 164 | (default is "admin bot captcha debug disable encryption env show sso test waf") 165 | --custom-values 166 | Values for custom parameters (default is "1 0 false off null true yes no") 167 | 168 | -t, --data-type 169 | Available: urlencode, json 170 | Can be detected automatically if --body is specified (default is "urlencode") 171 | -d, --delay [default: 0] 172 | -H Example: -H 'one:one' 'two:two' 173 | --http HTTP version. Supported versions: --http 1.1, --http 2 174 | -j, --joiner 175 | How to join parameter templates. Example: --joiner '&' 176 | Default: urlencoded - '&', json - ', ', header values - '; ' 177 | --learn-requests Set the custom number of learn requests. [default: 9] 178 | -m, --max 179 | Change the maximum number of parameters per request. 180 | (default is <= 256 for query, 64 for headers and 512 for body) 181 | -X, --method Multiple values are supported: -X GET POST 182 | -o, --output 183 | -O, --output-format standart, json, url, request [default: standart] 184 | -P, --param-template 185 | %k - key, %v - value. Example: --param-template 'user[%k]=%v' 186 | Default: urlencoded - <%k=%v>, json - <"%k":%v>, headers - <%k=%v> 187 | -p, --port Port to use with request file 188 | --progress-bar-len [default: 26] 189 | --proto Protocol to use with request file (default is "https") 190 | -x, --proxy 191 | --recursion-depth 192 | Check the same list of parameters with the found parameters until there are no new parameters to be found. 193 | Conflicts with --verify for now. 194 | --replay-proxy 195 | Request target with every found parameter via the replay proxy at the end. 196 | 197 | -r, --request The file with the raw http request 198 | --save-responses 199 | Save request and response to a directory when a parameter is found 200 | 201 | --split-by 202 | Split the request into lines by the provided sequence. By default splits by \r, \n and \r\n 203 | 204 | --timeout HTTP request timeout in seconds. [default: 15] 205 | -u, --url 206 | You can add a custom injection point with %s. 207 | Multiple urls and filenames are supported: 208 | -u filename.txt 209 | -u https://url1 http://url2 210 | -v, --verbose Verbose level 0/1/2 [default: 1] 211 | -w, --wordlist 212 | The file with parameters (leave empty to read from stdin) [default: ] 213 | 214 | -W, --workers 215 | The number of concurrent url checks. 216 | Use -W0 to run everything in parallel [default: 1] 217 | ``` 218 | 219 | # Wordlists 220 | Parameters: 221 | - [samlists](https://github.com/the-xentropy/samlists) 222 | - [arjun](https://github.com/s0md3v/Arjun/tree/master/arjun/db) 223 | 224 | Headers: 225 | - [Param Miner](https://github.com/danielmiessler/SecLists/tree/master/Discovery/Web-Content/BurpSuite-ParamMiner) 226 | 227 | # Burp Suite integration 228 | 229 | The burpsuite integration is done via the [send to](https://portswigger.net/bappstore/f089f1ad056545489139cb9f32900f8e) extension. 230 | 231 | ### Setting up 232 | 233 | 1. Launch Burp Suite and navigate to the 'Extender' tab. 234 | 2. Locate and install the 'Custom Send To' extension from the BApp Store. 235 | 3. Open the 'Send to' tab and click on the 'Add' button to configure the extension. 236 | 237 | Give a name to the entry and insert the following line into the command: 238 | 239 | ``` 240 | /path/to/x8 --progress-bar-len 20 -c 3 -r %R -w /path/to/wordlist --proto %T --port %P 241 | ``` 242 | 243 | You can also add your frequently used arguments like `--output-format`,`--replay-proxy`, `--recursion-depth`, .. 244 | 245 | **NOTE** if the progress bar doesn't work properly --- try to reducing the value of `--progress-bar-len`. 246 | 247 | Switch from Run in background to Run in terminal. 248 | 249 | ![image](https://user-images.githubusercontent.com/54232788/201471567-2a388157-e2f1-4d68-aebe-5ecc3c1090ee.png) 250 | 251 | If you encounter issues with font rendering in the terminal, you can adjust the `xterm` options in **Send to Miscellaneous Options**. Simply replace the existing content with `xterm -rv -fa 'Monospace' -fs 10 -hold -e %C`, or substitute `xterm` with your preferred terminal emulator. 252 | 253 | Now you can go to the proxy/repeater tab and send the request to the tool: 254 | 255 | ![image](https://user-images.githubusercontent.com/54232788/201518132-87fd0c40-5877-4f46-a036-590967759b3f.png) 256 | 257 | In the next dialog, you can modify the command and execute it in a new terminal window. 258 | 259 | ![image](https://user-images.githubusercontent.com/54232788/201518230-3d7959c4-3530-497d-9aca-b20de80321cb.png) 260 | 261 | After executing the command, a new terminal window will appear, displaying the running tool. 262 | 263 | ![image](https://user-images.githubusercontent.com/54232788/224473570-cabbd4ee-8c15-4a09-bc2a-c660c534a429.jpg) 264 | 265 | # Installation 266 | 267 | **NOTE**: Starting with v4.0.0, installing via `cargo install` uses the `crate` branch instead of `main`. This branch includes the original `reqwest` library that performs HTTP normalizations and prevents sending invalid requests. If you want to use the modified reqwest version without these limitations, I recommend installing via the `Releases` page or building the sources. 268 | 269 | - Docker 270 | - installation 271 | ```bash 272 | git clone https://github.com/Sh1Yo/x8 273 | cd x8 274 | docker build -t x8 . 275 | ``` 276 | - [usage](https://github.com/Sh1Yo/x8/pull/29) 277 | 278 | - Linux 279 | - from releases 280 | - from blackarch repositories (repositories should be installed) 281 | ```bash 282 | # pacman -Sy x8 283 | ``` 284 | - from source code (rust should be installed) 285 | ```bash 286 | git clone https://github.com/sh1yo/x8 287 | cd x8 288 | cargo build --release 289 | # move the binary to $PATH so you can use it without specifying the full path 290 | cp ./target/release/x8 /usr/local/bin 291 | # if it says that /usr/local/bin doesn't exists you can try 292 | # sudo cp ./target/release/x8 /usr/bin 293 | ``` 294 | - via cargo install 295 | ```bash 296 | cargo install x8 297 | ``` 298 | - Mac 299 | - from source code (rust should be installed) 300 | ```bash 301 | git clone https://github.com/sh1yo/x8 302 | cd x8 303 | cargo build --release 304 | # move the binary to $PATH so you can use it without specifying the full path 305 | cp ./target/release/x8 /usr/local/bin 306 | ``` 307 | - via cargo install 308 | ```bash 309 | cargo install x8 310 | ``` 311 | 312 | - Windows 313 | - from releases 314 | -------------------------------------------------------------------------------- /docs.md: -------------------------------------------------------------------------------- 1 | - [User Interface](#user-interface) 2 | - [Command line arguments](#command-line-arguments) 3 | - [http request from file](#http-request-from-file) 4 | - [http request from command-line arguments](#http-request-from-command-line-arguments-conflicts-with---request) 5 | - [Parameters](#parameters) 6 | - [Behavior](#behavior) 7 | - [Concurrency](#concurrency) 8 | - [Output](#output) 9 | 10 | 11 | ## User Interface 12 | 13 | Usually, the tool's output looks like this: 14 | 15 | ![img](https://sh1yo.art/images/si9kh81QRe4PF9JF7g-4D0jGJ0ZAeDAW) 16 | 17 | ## Command line arguments 18 | 19 | ### http request from file 20 | 21 | ``` 22 | -r --request 23 | ``` 24 | 25 | This option specifies the file containing the raw HTTP request. 26 | 27 | When using a request file, the tool does not add default headers such as `Accept` and `User-Agent` to the request. 28 | 29 | At present, the URL is created directly from the Host header, so it is not possible to set an arbitrary Host header from within a request file. If you want to set a different Host header, see the `-H` option in the [HTTP Request from Command Line Arguments](#http-request-from-command-line-arguments) category. 30 | 31 | ``` 32 | --proto 33 | ``` 34 | 35 | This argument is additional and required when using the `--request` option. 36 | 37 | Specify either `http` or `https`. 38 | 39 | ``` 40 | --split-by 41 | ``` 42 | 43 | This option specifies how to split the request file. By default, the `.lines()` method is used, which treats `\r`, `\n`, and `\r\n` as line separators. 44 | 45 | For example, to split only by `\n`, use `--split-by '\n'`. 46 | 47 | ### http request from command-line arguments [conflicts with -\-request] 48 | 49 | ``` 50 | -u --url 51 | ``` 52 | 53 | This option specifies the target URL. Multiple URLs can be provided using `-u https://example.com https://4rt.one`, or by using a filename: `-u targets.txt`. 54 | 55 | To specify an injection point, use `%s`. For example, `-u https://4rt.one?a=b` is equivalent to `-u https://4rt.one/?a=b&%s`. 56 | 57 | Supported variables include {{random}}. For instance, `-u https://4rt.one/?something={{random}}` will cause the something parameter to take on new values for every request. 58 | 59 | ``` 60 | -X --method 61 | ``` 62 | 63 | This option specifies the request method. 64 | 65 | An example with multiple values: `-X GET POST` 66 | 67 | ``` 68 | -b --body 69 | ``` 70 | 71 | This option specifies the request body. 72 | 73 | To specify an injection point, use `%s`. For example, `-b '{"some":"value"}'` is equivalent to `-b '{"some":"value", %s}'`. 74 | 75 | Supported variables include `{{random}}`. 76 | 77 | ``` 78 | -H 79 | ``` 80 | 81 | This option specifies the request headers. 82 | 83 | For example, `-H "User-Agent: Mozilla" "X-Something: awesome"`. 84 | 85 | You can overwrite the default Host header as well. 86 | 87 | **NOTE**: Overwriting the `Host` header works properly only with `HTTP/1.1` because there is no `Host` header for `HTTP/2`. Instead, for `HTTP/2`, there is a special `:authority` header, but the tool currently cannot change special `HTTP/2` headers. 88 | 89 | **NOTE**: You may encounter some case-related problems. The library that I am using for requests is `reqwest`. It capitalizes the first letter of the header name (or one after `-`) and lowers the rest for `HTTP/1.1`. However, for `HTTP/2` requests, `reqwest` lowers every header name (as per `HTTP/2` specs). 90 | 91 | ``` 92 | --http <1.1/2> 93 | ``` 94 | 95 | This option forces the use of a specific HTTP version. You can specify either `1.1` or `2`. 96 | 97 | For example, `--http 1.1` will force the use of `HTTP/1.1`, while `--http 2` will force the use of `HTTP/2`. 98 | 99 | ### Parameters 100 | 101 | The tool's primary purpose is to handle a wide range of situations. To accomplish this, several options have been added that provide precise control over how and where parameters are inserted. 102 | 103 | To insert parameters into specific locations, use the `%s` variable. 104 | 105 | ``` 106 | -P --param-template 107 | ``` 108 | 109 | Here, `%k` represents the key, and `%v` represents the value. 110 | 111 | For standard GET requests, the parameter template is typically `%k=%v`. 112 | 113 | Default values are `%k=%v` for URL-encoded data, `"%k":%v` for JSON, and `%k=%v` for header values. 114 | 115 | Examples: 116 | 117 | - To search for specific object fields: `-P user[%k]=%v` 118 | - To search for json array values: `-P "%k"`, with `--body '{"arr":[%s]}' --joiner ', '` 119 | 120 | 121 | ``` 122 | -j --joiner 123 | ``` 124 | 125 | This argument determines how to join parameters together. For ordinary GET requests, it's `&`. 126 | 127 | Default values: for urlencoded `&`, for JSON `,`, for header values `; ` 128 | 129 | - Custom made XML discovery format: `--body "%s" --joiner "\n" --param-template "<%k>%v"` 130 | 131 | 132 | ``` 133 | -t --data-type 134 | ``` 135 | 136 | Sometimes you need to tell the tool the data type. 137 | 138 | For example, when the body isn't provided with the `POST` method. By default, **urlencoded** format will be used. You can change this behavior with `-t json` 139 | 140 | ``` 141 | --encode 142 | ``` 143 | In some contexts, you may need to encode special characters. `&` becomes `%26` 144 | 145 | List of characters to encode: **["`<>&#;/=%]** 146 | 147 | For example, when you find an app that forwards a specific parameter to the backend: `-u 'https://4rt.one/v?uid=00000%26%s' --encode` 148 | 149 | `https://4rt.one/v?uid=%26param%3dvalue` -> makes request to -> `http://internal/secret?uid=¶m=value` 150 | 151 | ``` 152 | --custom-parameters --custom-values 153 | ``` 154 | 155 | Some parameters can often have non-random values like `debug=1`. The tool automatically checks for these cases, but you can overwrite the default values. 156 | 157 | Default values: 158 | 159 | `--custom-parameters admin bot captcha debug disable encryption env show sso test waf` 160 | 161 | `--custom-values 1 0 false off null true yes no` 162 | 163 | *Usually, adding an additional custom parameter is free, while adding a custom value costs 1 request per value.* 164 | 165 | ``` 166 | --disable-custom-parameters 167 | ``` 168 | 169 | Disables checking for custom parameters by default. 170 | 171 | ``` 172 | -m --max 173 | ``` 174 | 175 | Determines how many parameters to send in every request. 176 | 177 | By default: for query parameters, it starts with 128 and tries to increase up to 256. With v4.2.0, the logic was improved and the value may even be less than 128. For headers and header values, the default is 64. For the body, the default is 512. 178 | 179 | ### Behavior 180 | 181 | ``` 182 | --headers 183 | ``` 184 | 185 | Search for headers. By default, the tool sends 64 headers per requests, but this can be configured with the `-m` option. 186 | 187 | **Note**: You may encounter all the limitations described in `-H` from [HTTP Request From Command-Line Arguments](#http-request-from-command-line-arguments) section. 188 | 189 | ``` 190 | --invert 191 | ``` 192 | 193 | Sometimes you may need to send parameters via the body with the `GET` method or via query with the `POST` method. By default, parameters are sent within the request body only with the `PUT` and `POST` methods, but it can be overwritten with the `--invert` option. 194 | 195 | ``` 196 | --recursion-depth [default: 1] 197 | ``` 198 | 199 | Checks the same list of parameters over and over, adding found parameters every run. 200 | 201 | *Only parameters that don't change the page's code are added to the next run.* 202 | 203 | ``` 204 | --reflected-only 205 | ``` 206 | 207 | Search only for reflected parameters to reduce the amount of sent requests. 208 | 209 | ``` 210 | --strict 211 | ``` 212 | 213 | Do not report parameters that change the same part of the page. This helps to get rid of mass false positives, such as when all the parameters containing `admin` cause page differences. Note that this can lead to a few false negatives as well. In the future, this option will be replaced with a bit better logic. 214 | 215 | ### Concurrency 216 | 217 | Implemented using async/awaits. 218 | 219 | ``` 220 | -W --workers [default: 1] 221 | ``` 222 | 223 | This specifies the number of concurrent URL checks. 224 | 225 | `-W 0` -- checks all URLs in parallel. 226 | 227 | ``` 228 | --one-worker-per-host 229 | ``` 230 | 231 | This option only checks URLs with different hosts in parallel. 232 | 233 | **Note**: This option does not increase the number of workers if there are fewer workers than hosts. You can use `-W 0` for one **worker** per **host**. 234 | 235 | ``` 236 | -c --concurrency [default: 1] 237 | ``` 238 | 239 | This specifies the number of concurrent jobs for each worker. 240 | 241 | ### Output 242 | 243 | ``` 244 | -v --verbose <0/1/2> [default: 1] 245 | ``` 246 | 247 | This option determines how much information to print to the console. 248 | 249 | The output also depends on the number of parallel URL checks. 250 | 251 | - 0 --- prints only the initial configuration, URL configuration, and their found parameters. The progress bar remains but can be disabled with `--disable-progress-bar`. 252 | - 1 --- 0 + prints every discovered parameter's kind if only one URL is being checked in parallel. 253 | - 2 --- 0 + prints every discovered parameter's kind always. 254 | 255 | ``` 256 | -o --output 257 | ``` 258 | 259 | This option specifies the file where the final output is written. 260 | 261 | By default, the file overwrites unless `--append` is provided. 262 | 263 | The file is dynamically populated unless the JSON output is used. 264 | 265 | ``` 266 | -O --output-format 267 | ``` 268 | 269 | This option specifies the output format for the final message about found parameters. 270 | 271 | If `--output` is defined, the same message is printed to the file. 272 | 273 | **standart**: ` % ` 274 | 275 | **json**: 276 | ```json 277 | [ 278 | { 279 | "method": "", 280 | "url": "", 281 | "status": , 282 | "size": , 283 | "found_params": [ 284 | { 285 | "name": "", 286 | "value": "", 287 | "diffs": "", 288 | "status": , 289 | "size": , 290 | "reason_kind": "" 291 | } 292 | ], 293 | "injection_place": "" 294 | } 295 | ] 296 | ``` 297 | 298 | reason_kind can take on 4 values: 299 | 300 | - Code --- the parameter changes the page's code. 301 | - Text --- the parameter changes the page's body or headers. 302 | - Reflected --- the parameter reflects on the page different amount of times (compared to non-existing parameters). 303 | - NotReflected --- the parameter causes other parameters to reflect different amount of times. 304 | 305 | **url**: `?` 306 | 307 | **request**: The http request with parameters. Parameter values can be either random or specific like 'true'. 308 | 309 | ``` 310 | --remove-empty 311 | ``` 312 | 313 | This option excludes entries without found parameters from the output file. -------------------------------------------------------------------------------- /src/.gitattributes: -------------------------------------------------------------------------------- 1 | Cargo.toml merge=ours 2 | Cargo.lock merge=ours 3 | -------------------------------------------------------------------------------- /src/config/args.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | config::{ 3 | structs::Config, 4 | utils::{convert_to_string_if_some, parse_request}, 5 | }, 6 | network::utils::{DataType, Headers}, 7 | }; 8 | use clap::{crate_version, App, AppSettings, Arg}; 9 | use std::{collections::HashMap, error::Error, fs, io::{self, Write}}; 10 | use tokio::time::Duration; 11 | use url::Url; 12 | 13 | use super::utils::{read_urls_if_possible, mimic_browser_headers, add_default_headers}; 14 | 15 | pub fn get_config() -> Result> { 16 | let app = App::new("x8") 17 | .setting(AppSettings::ArgRequiredElseHelp) 18 | .version(crate_version!()) 19 | .author("sh1yo ") 20 | .about("Hidden parameters discovery suite") 21 | .arg(Arg::with_name("url") 22 | .short("u") 23 | .long("url") 24 | .help("You can add a custom injection point with %s.\nMultiple urls and filenames are supported:\n-u filename.txt\n-u https://url1 http://url2") 25 | .takes_value(true) 26 | .min_values(1) 27 | .conflicts_with("request") 28 | ) 29 | .arg(Arg::with_name("request") 30 | .short("r") 31 | .long("request") 32 | .help("The file with the raw http request") 33 | .takes_value(true) 34 | .conflicts_with("url") 35 | ) 36 | .arg(Arg::with_name("proto") 37 | .long("proto") 38 | .help("Protocol to use with request file (default is \"https\")") 39 | .takes_value(true) 40 | .requires("request") 41 | .conflicts_with("url") 42 | ) 43 | .arg(Arg::with_name("port") 44 | .long("port") 45 | .short("-p") 46 | .help("Port to use with request file") 47 | .takes_value(true) 48 | .requires("request") 49 | .conflicts_with("url") 50 | ) 51 | .arg(Arg::with_name("split-by") 52 | .long("split-by") 53 | .help("Split the request into lines by the provided sequence. By default splits by \\r, \\n and \\r\\n") 54 | .takes_value(true) 55 | .requires("request") 56 | .conflicts_with("url") 57 | ) 58 | .arg( 59 | Arg::with_name("wordlist") 60 | .short("w") 61 | .long("wordlist") 62 | .help("The file with parameters (leave empty to read from stdin)") 63 | .default_value("") 64 | .takes_value(true), 65 | ) 66 | .arg( 67 | Arg::with_name("parameter-template") 68 | .short("P") 69 | .long("param-template") 70 | .help("%k - key, %v - value. Example: --param-template 'user[%k]=%v'\nDefault: urlencoded - <%k=%v>, json - <\"%k\":%v>, headers - <%k=%v>") 71 | .takes_value(true), 72 | ) 73 | .arg( 74 | Arg::with_name("joiner") 75 | .short("j") 76 | .long("joiner") 77 | .help("How to join parameter templates. Example: --joiner '&'\nDefault: urlencoded - '&', json - ', ', header values - '; '") 78 | .takes_value(true), 79 | ) 80 | .arg( 81 | Arg::with_name("body") 82 | .short("b") 83 | .long("body") 84 | .help("Example: --body '{\"x\":{%s}}'\nAvailable variables: {{random}}") 85 | .value_name("body") 86 | .conflicts_with("request") 87 | ) 88 | .arg( 89 | Arg::with_name("data-type") 90 | .short("t") 91 | .long("data-type") 92 | .help("Available: urlencode, json\nCan be detected automatically if --body is specified (default is \"urlencode\")") 93 | .value_name("data-type") 94 | ) 95 | .arg( 96 | Arg::with_name("proxy") 97 | .short("x") 98 | .long("proxy") 99 | .value_name("proxy") 100 | .takes_value(true) 101 | ) 102 | .arg( 103 | Arg::with_name("burp-proxy") 104 | .short("B") 105 | .help("Equal to -x http://localhost:8080") 106 | .conflicts_with("proxy") 107 | ) 108 | .arg( 109 | Arg::with_name("delay") 110 | .short("d") 111 | .long("delay") 112 | .value_name("Delay between requests in milliseconds") 113 | .default_value("0") 114 | .takes_value(true) 115 | ) 116 | .arg( 117 | Arg::with_name("output") 118 | .short("o") 119 | .long("output") 120 | .value_name("file") 121 | .takes_value(true) 122 | ) 123 | .arg( 124 | Arg::with_name("output-format") 125 | .short("O") 126 | .long("output-format") 127 | .help("standart, json, url, request") 128 | .default_value("standart") 129 | .takes_value(true) 130 | ) 131 | .arg( 132 | Arg::with_name("append") 133 | .long("append") 134 | .help("Append to the output file instead of overwriting it.") 135 | ) 136 | .arg( 137 | Arg::with_name("remove-empty") 138 | .long("remove-empty") 139 | .requires("output") 140 | .help("Skip writing to file outputs of url:method pairs without found parameters") 141 | ) 142 | .arg( 143 | Arg::with_name("method") 144 | .short("X") 145 | .long("method") 146 | .value_name("methods") 147 | .help("Multiple values are supported: -X GET POST") 148 | .takes_value(true) 149 | .min_values(1) 150 | .conflicts_with("request") 151 | ) 152 | .arg( 153 | Arg::with_name("headers") 154 | .short("H") 155 | .help("Example: -H 'one:one' 'two:two'") 156 | .takes_value(true) 157 | .min_values(1) 158 | .conflicts_with("request") 159 | ) 160 | .arg( 161 | Arg::with_name("invert") 162 | .long("invert") 163 | .help("By default, parameters are sent within the body only in case POST,PUT,PATCH,DELETE methods are used. 164 | It's possible to overwrite this behavior by specifying the option") 165 | .conflicts_with("headers-discovery") 166 | ) 167 | .arg( 168 | Arg::with_name("headers-discovery") 169 | .long("headers") 170 | .help("Switch to header discovery mode.\nNOTE Content-Length and Host headers are automatically removed from the list") 171 | .conflicts_with("invert") 172 | .conflicts_with("param-template") 173 | ) 174 | .arg( 175 | Arg::with_name("force") 176 | .long("force") 177 | .help("Force searching for parameters on pages > 25MB. Remove an error in case there's 1 worker with --one-worker-per-host option.") 178 | ) 179 | .arg( 180 | Arg::with_name("disable-custom-parameters") 181 | .long("disable-custom-parameters") 182 | .help("Do not automatically check parameters like admin=true") 183 | ) 184 | .arg( 185 | Arg::with_name("disable-colors") 186 | .long("disable-colors") 187 | ) 188 | .arg( 189 | Arg::with_name("force-enable-colors") 190 | .long("force-enable-colors") 191 | ) 192 | .arg( 193 | Arg::with_name("disable-trustdns") 194 | .long("disable-trustdns") 195 | .help("Can solve some dns related problems") 196 | ) 197 | .arg( 198 | Arg::with_name("disable-progress-bar") 199 | .long("disable-progress-bar") 200 | ) 201 | .arg( 202 | Arg::with_name("progress-bar-len") 203 | .long("progress-bar-len") 204 | .default_value("26") 205 | ) 206 | .arg( 207 | Arg::with_name("replay-once") 208 | .long("replay-once") 209 | .help("If a replay proxy is specified, send all found parameters within one request.") 210 | .requires("replay-proxy") 211 | ) 212 | .arg( 213 | Arg::with_name("replay-proxy") 214 | .takes_value(true) 215 | .long("replay-proxy") 216 | .help("Request target with every found parameter via the replay proxy at the end.") 217 | ) 218 | .arg( 219 | Arg::with_name("custom-parameters") 220 | .long("custom-parameters") 221 | .help("Check these parameters with non-random values like true/false yes/no\n(default is \"admin bot captcha debug disable encryption env show sso test waf\")") 222 | .takes_value(true) 223 | .min_values(1) 224 | .conflicts_with("disable-custom-parameters") 225 | ) 226 | .arg( 227 | Arg::with_name("custom-values") 228 | .long("custom-values") 229 | .help("Values for custom parameters (default is \"1 0 false off null true yes no\")") 230 | .takes_value(true) 231 | .min_values(1) 232 | .conflicts_with("disable-custom-parameters") 233 | ) 234 | .arg( 235 | Arg::with_name("follow-redirects") 236 | .long("follow-redirects") 237 | .short("L") 238 | .help("Follow redirections") 239 | ) 240 | .arg( 241 | Arg::with_name("encode") 242 | .long("encode") 243 | .help("Encodes query or body before making a request, i.e & -> %26, = -> %3D\nList of chars to encode: \", `, , <, >, &, #, ;, /, =, %") 244 | ) 245 | .arg( 246 | Arg::with_name("strict") 247 | .long("strict") 248 | .help("Only report parameters that have changed the different parts of a page") 249 | ) 250 | .arg( 251 | Arg::with_name("test") 252 | .long("test") 253 | .help("Prints request and response") 254 | ) 255 | .arg( 256 | Arg::with_name("verbose") 257 | .long("verbose") 258 | .short("v") 259 | .help("Verbose level 0/1/2") 260 | .default_value("1") 261 | .takes_value(true) 262 | ) 263 | .arg( 264 | Arg::with_name("save-responses") 265 | .long("save-responses") 266 | .help("Save request and response to a directory when a parameter is found") 267 | .takes_value(true) 268 | ) 269 | .arg( 270 | Arg::with_name("learn-requests-count") 271 | .long("learn-requests") 272 | .help("Set the custom number of learn requests.") 273 | .default_value("9") 274 | .takes_value(true) 275 | ) 276 | .arg( 277 | Arg::with_name("recursion-depth") 278 | .long("recursion-depth") 279 | .help("Check the same list of parameters with the found parameters until there are no new parameters to be found. 280 | Conflicts with --verify for now.") 281 | .takes_value(true) 282 | .conflicts_with("verify") 283 | ) 284 | .arg( 285 | Arg::with_name("max") 286 | .short("m") 287 | .long("max") 288 | .help("Change the maximum number of parameters per request.\n(default is <= 256 for query, 64 for headers and 512 for body)") 289 | .takes_value(true) 290 | ) 291 | .arg( 292 | Arg::with_name("timeout") 293 | .long("timeout") 294 | .help("HTTP request timeout in seconds.") 295 | .default_value("15") 296 | .takes_value(true) 297 | ) 298 | .arg( 299 | Arg::with_name("concurrency") 300 | .short("c") 301 | .help("The number of concurrent requests per url") 302 | .default_value("1") 303 | .takes_value(true) 304 | ) 305 | .arg( 306 | Arg::with_name("workers") 307 | .short("W") 308 | .long("workers") 309 | .help("The number of concurrent url checks.\nUse -W0 to run everything in parallel") 310 | .default_value("1") 311 | .takes_value(true) 312 | ) 313 | .arg( 314 | Arg::with_name("verify") 315 | .long("verify") 316 | .help("Verify found parameters.") 317 | ) 318 | .arg( 319 | Arg::with_name("reflected-only") 320 | .long("reflected-only") 321 | .help("Disable page comparison and search for reflected parameters only.") 322 | ) 323 | .arg( 324 | Arg::with_name("one-worker-per-host") 325 | .long("one-worker-per-host") 326 | .help("Multiple urls with the same host will be checked one after another,\nwhile urls with different hosts - are in parallel.\nDoesn't increase the number of workers") 327 | ) 328 | .arg( 329 | Arg::with_name("mimic-browser") 330 | .long("mimic-browser") 331 | .help("Add default headers that browsers usually set.") 332 | .conflicts_with("request") 333 | ) 334 | .arg( 335 | Arg::with_name("http") 336 | .long("http") 337 | .help("HTTP version. Supported versions: --http 1.1, --http 2") 338 | .takes_value(true) 339 | ).arg( 340 | Arg::with_name("check-binary") 341 | .long("check-binary") 342 | .help("Check the body of responses with binary content types") 343 | ).arg( 344 | Arg::with_name("cookies") 345 | .long("cookies") 346 | .help("Shortcut for adding injection point to cookies") 347 | ).arg( 348 | Arg::with_name("remove-banner") 349 | .long("remove-banner") 350 | .help("Do not print initial banner") 351 | ); 352 | 353 | let args = app.clone().get_matches(); 354 | 355 | if args.value_of("url").is_none() && args.value_of("request").is_none() { 356 | Err("A target was not provided")?; 357 | } 358 | 359 | // parse numbers 360 | let delay = Duration::from_millis(args.value_of("delay").unwrap().parse()?); 361 | 362 | let learn_requests_count = args.value_of("learn-requests-count").unwrap().parse()?; 363 | let concurrency = args.value_of("concurrency").unwrap().parse()?; 364 | let workers = args.value_of("workers").unwrap().parse()?; 365 | let verbose = args.value_of("verbose").unwrap().parse()?; 366 | let timeout = args.value_of("timeout").unwrap().parse()?; 367 | let recursion_depth = args.value_of("recursion-depth").unwrap_or("0").parse()?; 368 | let progress_bar_len = args.value_of("progress-bar-len").unwrap().parse()?; 369 | 370 | let max: Option = if args.is_present("max") { 371 | Some(args.value_of("max").unwrap().parse()?) 372 | } else { 373 | None 374 | }; 375 | 376 | if workers == 1 && args.is_present("one-worker-per-host") && !args.is_present("force") { 377 | Err("The --one-worker-per-host option doesn't increase the amount of workers. \ 378 | So there's no point in --one-worker-per-host with 1 worker. \ 379 | Increase the amount of workers to remove the error or use --force.")?; 380 | } 381 | 382 | // try to read request file 383 | let request = match args.value_of("request") { 384 | Some(val) => fs::read_to_string(val)?, 385 | None => String::new(), 386 | }; 387 | 388 | let data_type = match args.value_of("data-type") { 389 | Some(val) => { 390 | if val == "json" { 391 | Some(DataType::Json) 392 | } else if val == "urlencoded" { 393 | Some(DataType::Urlencoded) 394 | } else { 395 | Err("Incorrect --data-type specified")? 396 | } 397 | } 398 | None => None 399 | }; 400 | 401 | // parse the default request information 402 | // either via the request file or via provided parameters 403 | let (methods, urls, mut headers, body, data_type, http_version) = if !request.is_empty() { 404 | // if the request file is specified - get protocol (https/http) from args, specify scheme and port, and parse request file 405 | let proto = args 406 | .value_of("proto") 407 | .unwrap_or("https") 408 | .to_string(); 409 | 410 | let scheme = proto.replace("://", ""); 411 | 412 | let port: Option = if args.value_of("port").is_some() { 413 | Some(args.value_of("port").unwrap().parse()?) 414 | } else { 415 | None 416 | }; 417 | 418 | parse_request(&request, &scheme, port, data_type, args.value_of("split-by"))? 419 | } else { 420 | // parse everything from user-supplied command line arguments 421 | let methods = if args.is_present("method") { 422 | args.values_of("method") 423 | .unwrap() 424 | .map(|x| x.to_string()) 425 | .collect::>() 426 | } else { 427 | vec!["GET".to_string()] 428 | }; 429 | 430 | let mut headers: HashMap<&str, String> = HashMap::new(); 431 | 432 | if let Some(val) = args.values_of("headers") { 433 | for header in val { 434 | let mut k_v = header.split(':'); 435 | let key = match k_v.next() { 436 | Some(val) => val, 437 | None => Err("Unable to parse headers")?, 438 | }; 439 | let value = [ 440 | match k_v.next() { 441 | Some(val) => val.trim().to_owned(), 442 | None => Err("Unable to parse headers")?, 443 | }, 444 | k_v.map(|x| ":".to_owned() + x).collect(), 445 | ] 446 | .concat(); 447 | 448 | headers.insert(key, value); 449 | } 450 | }; 451 | 452 | // set default headers if weren't specified by a user. 453 | let headers = if args.is_present("mimic-browser") { 454 | mimic_browser_headers(headers) 455 | } else { 456 | add_default_headers(headers) 457 | }; 458 | 459 | // TODO replace with ".parse()" or sth like it 460 | let data_type = match data_type { 461 | Some(val) => { 462 | Some(val) 463 | } 464 | None => if headers.get_value_case_insensitive("content-type") == Some("application/json".to_string()) { 465 | Some(DataType::ProbablyJson) 466 | } else { 467 | None 468 | }, 469 | }; 470 | 471 | let http_version = if args.value_of("http").is_some() { 472 | match args.value_of("http").unwrap() { 473 | "1.1" => Some(http::Version::HTTP_11), 474 | "2" => Some(http::Version::HTTP_2), 475 | _ => { 476 | writeln!( 477 | io::stdout(), 478 | "[#] Incorrect http version provided. The argument is ignored" 479 | ).ok(); 480 | None 481 | } 482 | } 483 | } else { 484 | None 485 | }; 486 | 487 | let urls = args 488 | .values_of("url") 489 | .unwrap(); 490 | 491 | let urls = if urls.len() == 1 && !urls.clone().any(|x| x.contains("://")) { 492 | // it can be a file 493 | match read_urls_if_possible(urls.clone().next().unwrap())? { 494 | Some(urls) => urls, 495 | None => Err("The provided --url value is neither url nor a filename.")? 496 | } 497 | } else { 498 | urls.map(|x| x.to_string()).collect() 499 | }; 500 | 501 | let urls = urls.iter().map(|x| Url::parse(x)) 502 | .collect::>>(); 503 | 504 | // in case there's at least a single wrong url -- return with an error 505 | if urls.iter().any(|x| x.is_err()) { 506 | for err_url in urls.iter().filter(|x| x.is_err()) { 507 | err_url.to_owned()?; 508 | } 509 | unreachable!(); 510 | } else { 511 | ( 512 | methods, 513 | urls.iter() 514 | .map(|x| x.as_ref().unwrap().to_string()) 515 | .collect::>(), 516 | headers, 517 | args.value_of("body").unwrap_or("").to_string(), 518 | data_type, 519 | http_version 520 | ) 521 | } 522 | }; 523 | 524 | // generate custom param values like admin=true 525 | let custom_keys: Vec = match args.values_of("custom-parameters") { 526 | Some(val) => val.map(|x| x.to_string()).collect(), 527 | None => [ 528 | "admin", 529 | "bot", 530 | "captcha", 531 | "debug", 532 | "disable", 533 | "encryption", 534 | "env", 535 | "show", 536 | "sso", 537 | "test", 538 | "waf", 539 | ] 540 | .iter() 541 | .map(|x| x.to_string()) 542 | .collect(), 543 | }; 544 | 545 | let custom_values: Vec = match args.values_of("custom-values") { 546 | Some(val) => val.map(|x| x.to_string()).collect(), 547 | None => ["1", "0", "false", "off", "null", "true", "yes", "no"] 548 | .iter() 549 | .map(|x| x.to_string()) 550 | .collect(), 551 | }; 552 | 553 | let mut custom_parameters: HashMap> = 554 | HashMap::with_capacity(custom_keys.len()); 555 | for key in custom_keys.iter() { 556 | let mut values: Vec = Vec::with_capacity(custom_values.len()); 557 | for value in custom_values.iter() { 558 | values.push(value.to_string()); 559 | } 560 | custom_parameters.insert(key.to_string(), values); 561 | } 562 | 563 | // disable colors 564 | if args.is_present("disable-colors") { 565 | colored::control::set_override(false); 566 | } 567 | 568 | // force enable colors to preseve colors while redirecting output 569 | if args.is_present("force-enable-colors") { 570 | colored::control::set_override(true); 571 | } 572 | 573 | // decrease verbose by 1 in case > 1 url is being checked in parallel 574 | // this behavior is explained in docs 575 | let verbose = if verbose > 0 && !(workers == 1 || urls.len() == 1) { 576 | verbose - 1 577 | } else { 578 | verbose 579 | }; 580 | 581 | let proxy = if args.is_present("burp-proxy") { 582 | "http://localhost:8080".to_string() 583 | } else { 584 | args.value_of("proxy").unwrap_or("").to_string() 585 | }; 586 | 587 | if args.is_present("cookies") { 588 | if let Some(index) = headers.get_index_case_insensitive("cookie") { 589 | headers[index] = (headers[index].0.clone(), headers[index].1.clone()+";%s") 590 | } else { 591 | headers.push(("Cookie".to_string(), "%s".to_string())); 592 | } 593 | } 594 | 595 | // TODO maybe replace empty with None 596 | Ok(Config { 597 | urls, 598 | methods, 599 | wordlist: args.value_of("wordlist").unwrap_or("").to_string(), 600 | custom_parameters, 601 | proxy, 602 | replay_proxy: args.value_of("replay-proxy").unwrap_or("").to_string(), 603 | replay_once: args.is_present("replay-once"), 604 | output_file: args.value_of("output").unwrap_or("").to_string(), 605 | save_responses: args.value_of("save-responses").unwrap_or("").to_string(), 606 | output_format: args.value_of("output-format").unwrap_or("").to_string(), 607 | append: args.is_present("append"), 608 | remove_empty: args.is_present("remove-empty"), 609 | force: args.is_present("force"), 610 | strict: args.is_present("strict"), 611 | disable_progress_bar: args.is_present("disable-progress-bar"), 612 | progress_bar_len, 613 | follow_redirects: args.is_present("follow-redirects"), 614 | test: args.is_present("test"), 615 | verbose, 616 | learn_requests_count, 617 | concurrency, 618 | workers, 619 | timeout, 620 | recursion_depth, 621 | verify: args.is_present("verify"), 622 | reflected_only: args.is_present("reflected-only"), 623 | http_version, 624 | template: convert_to_string_if_some(args.value_of("parameter-template")), 625 | joiner: convert_to_string_if_some(args.value_of("joiner")), 626 | encode: args.is_present("encode"), 627 | disable_custom_parameters: args.is_present("disable-custom-parameters"), 628 | one_worker_per_host: args.is_present("one-worker-per-host"), 629 | invert: args.is_present("invert"), 630 | headers_discovery: args.is_present("headers-discovery") || args.is_present("cookies"), 631 | body, 632 | delay, 633 | custom_headers: headers 634 | .iter() 635 | .map(|(k, v)| (k.to_string(), v.to_string())) 636 | .collect(), 637 | data_type, 638 | max, 639 | disable_colors: args.is_present("disable-colors"), 640 | remove_banner: args.is_present("remove-banner"), 641 | disable_trustdns: args.is_present("disable-trustdns"), 642 | check_binary: args.is_present("check-binary"), 643 | }) 644 | } 645 | -------------------------------------------------------------------------------- /src/config/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod args; 2 | pub mod structs; 3 | pub mod utils; 4 | -------------------------------------------------------------------------------- /src/config/structs.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, time::Duration}; 2 | 3 | use crate::network::utils::DataType; 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct Config { 7 | /// default urls without any changes (except from when used from request file, maybe change this logic TODO) 8 | pub urls: Vec, 9 | 10 | /// a list of methods to check urls with 11 | pub methods: Vec, 12 | 13 | /// custom user supplied headers or default ones 14 | pub custom_headers: Vec<(String, String)>, 15 | 16 | /// how much to sleep between requests in millisecs 17 | pub delay: Duration, 18 | 19 | /// user supplied wordlist file 20 | pub wordlist: String, 21 | 22 | /// max amount of parameters to send per request. 23 | /// Can be specified by user otherwise detects automatically based on the request method 24 | pub max: Option, 25 | 26 | /// parameter template, for example %k=%v 27 | pub template: Option, 28 | 29 | /// how to join parameters, for example '&' 30 | pub joiner: Option, 31 | 32 | /// whether to encode the query like param1=value1¶m2=value2 -> param1%3dvalue1%26param2%3dvalue2 33 | pub encode: bool, 34 | 35 | /// default body 36 | pub body: String, 37 | 38 | /// Json type handles differently because values like null, true, ints needs to be sent without quotes 39 | /// Probably better to replace with just isJson for now.. 40 | pub data_type: Option, 41 | 42 | /// whether to include parameters like debug=true to the list 43 | pub disable_custom_parameters: bool, 44 | 45 | /// proxy server with schema or http:// by default. 46 | pub proxy: String, 47 | 48 | /// file to output 49 | pub output_file: String, 50 | 51 | /// whether to append to the output file instead of overwriting 52 | pub append: bool, 53 | 54 | /// do not print outputs of pairs url:method without found parameters 55 | pub remove_empty: bool, 56 | 57 | /// output format for file & stdout outputs 58 | pub output_format: String, 59 | 60 | /// a directory for saving request & responses with found parameters 61 | pub save_responses: String, 62 | 63 | /// ignore some custom errors like when page's size > MAX_PAGE_SIZE 64 | pub force: bool, 65 | 66 | /// only report parameteres with different "diffs" 67 | /// in case a few parameters change the same part of a page - only one of them will be saved 68 | /// greatly reduces false positives and a bit increases false negatives 69 | pub strict: bool, 70 | 71 | /// custom parameters to check like 72 | pub custom_parameters: HashMap>, 73 | 74 | pub disable_progress_bar: bool, 75 | 76 | /// the size of progress bar in chars 77 | pub progress_bar_len: usize, 78 | 79 | /// proxy to resend requests with found parameter 80 | pub replay_proxy: String, 81 | 82 | /// whether to resend the request once with all parameters or once per every parameter 83 | pub replay_once: bool, 84 | 85 | /// print request & response and exit. 86 | /// can be useful for checking whether the program parsed the input parameters successfully 87 | pub test: bool, 88 | 89 | /// 0 - print only critical errors and output 90 | /// 1 - print intermediate results and progress bar 91 | pub verbose: usize, 92 | 93 | /// determines how much learning requests should be made on the start 94 | /// doesn't include first two requests made for cookies and initial response 95 | pub learn_requests_count: usize, 96 | 97 | /// checks the same list of parameters with the found parameters until there are no new parameters to be found. 98 | /// conflicts with --verify for now. Will be updated in the future. 99 | pub recursion_depth: usize, 100 | 101 | /// amount of concurrent requests per url 102 | pub concurrency: usize, 103 | 104 | /// amount of concurrent url checks 105 | pub workers: usize, 106 | 107 | /// http request timeout in seconds 108 | pub timeout: usize, 109 | 110 | /// whether the verify found parameters one time more. 111 | /// in future wil check for _false_potives like when every parameter that starts with _ is found 112 | pub verify: bool, 113 | 114 | /// check only for reflected parameters in order to decrease the amount of requests 115 | /// usually makes 2+learn_request_count+words/max requests 116 | /// but in rare cases its number may be higher 117 | pub reflected_only: bool, 118 | 119 | pub one_worker_per_host: bool, 120 | 121 | pub http_version: Option, 122 | 123 | /// by default parameters are sent within the body only in case PUT or POST methods are used. 124 | /// it's possible to overwrite this behavior by specifying this option 125 | pub invert: bool, 126 | 127 | /// true in case the injection points is within the header or the headers are injection point itself 128 | pub headers_discovery: bool, 129 | 130 | pub follow_redirects: bool, 131 | 132 | pub disable_colors: bool, 133 | 134 | pub remove_banner: bool, 135 | 136 | pub disable_trustdns: bool, 137 | 138 | /// check body of responses with binary content type 139 | pub check_binary: bool, 140 | } 141 | -------------------------------------------------------------------------------- /src/config/utils.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::File, 3 | collections::HashMap, 4 | error::Error, 5 | io::{self, BufRead, Write}, 6 | }; 7 | 8 | use colored::Colorize; 9 | 10 | use crate::network::utils::DataType; 11 | 12 | use super::structs::Config; 13 | 14 | /// shorcut to convert Option<&str> to Option to be able to return it from the function 15 | pub(super) fn convert_to_string_if_some(el: Option<&str>) -> Option { 16 | if let Some(val) = el { 17 | Some(val.to_string()) 18 | } else { 19 | None 20 | } 21 | } 22 | 23 | /// parse request from the request file 24 | pub(super) fn parse_request<'a>( 25 | request: &'a str, 26 | scheme: &str, 27 | port: Option, 28 | mut data_type: Option, 29 | split_by: Option<&str>, 30 | ) -> Result< 31 | ( 32 | Vec, // method 33 | Vec, // url 34 | Vec<(String, String)>, // headers 35 | String, // body 36 | Option, 37 | Option, // http version 38 | ), 39 | Box, 40 | > { 41 | // request by lines 42 | let lines = if let Some(val) = split_by { 43 | request 44 | .split(&val.replace("\\r", "\r").replace("\\n", "\n")) 45 | .collect::>() 46 | } else { 47 | request.lines().collect::>() 48 | }; 49 | let mut lines = lines.iter(); 50 | 51 | let mut headers: Vec<(String, String)> = Vec::new(); 52 | let mut host = String::new(); 53 | 54 | // parse the first line 55 | let mut firstline = lines.next().ok_or("Unable to parse firstline")?.split(' '); 56 | let method = firstline 57 | .next() 58 | .ok_or("Unable to parse method")? 59 | .to_string(); 60 | let path = firstline.next().ok_or("Unable to parse path")?.to_string(); //include ' ' in path too? 61 | let http2 = firstline 62 | .next() 63 | .ok_or("Unable to parse http version")? 64 | .contains("HTTP/2"); 65 | 66 | // parse headers 67 | for line in lines.by_ref() { 68 | if line.is_empty() { 69 | break; 70 | } 71 | 72 | let mut k_v = line.split(':'); 73 | let key = k_v.next().ok_or("Unable to parse header key")?; 74 | let value: String = [ 75 | k_v.next() 76 | .ok_or("Unable to parse header value")? 77 | .trim() 78 | .to_owned(), 79 | k_v.map(|x| ":".to_owned() + x).collect(), 80 | ] 81 | .concat(); 82 | 83 | match key.to_lowercase().as_str() { 84 | "content-type" => { 85 | if value.contains("json") && data_type.is_none() { 86 | data_type = Some(DataType::ProbablyJson) 87 | } 88 | } 89 | "host" => { 90 | host = value.clone(); 91 | // host header in http2 breaks the h2 lib for now 92 | if http2 { 93 | continue; 94 | } 95 | } 96 | // breaks h2 too 97 | // TODO maybe add an option to keep request as it is without removing anything 98 | "content-length" => continue, 99 | _ => (), 100 | }; 101 | 102 | headers.push((key.to_string(), value)); 103 | } 104 | 105 | let mut body = lines.next().unwrap_or(&"").to_string(); 106 | for part in lines { 107 | if !part.is_empty() { 108 | body.push_str("\r\n"); 109 | body.push_str(part); 110 | } 111 | } 112 | 113 | // port from the --port argument has a priority against port within the host header 114 | let (host, port) = if port.is_some() { 115 | (host.split(':').next().unwrap().to_string(), port.unwrap()) 116 | } else if port.is_none() && host.contains(':') { 117 | let mut host = host.split(':'); 118 | (host.next().unwrap().to_string(), host.next().unwrap().parse()?) 119 | } else { 120 | // neither --port nor port within the host header were specified 121 | if scheme == "http" { 122 | (host, 80u16) 123 | } else { 124 | (host, 443u16) 125 | } 126 | }; 127 | 128 | Ok(( 129 | vec![method], 130 | vec![format!("{}://{}:{}{}", scheme, host, port, path)], 131 | headers, 132 | body, 133 | data_type, 134 | if http2 { Some(http::Version::HTTP_2) } else { Some(http::Version::HTTP_11) } 135 | )) 136 | } 137 | 138 | pub fn write_banner_config(config: &Config, params: &Vec) { 139 | let mut output = format!( 140 | "{}: {}\n{}: {}\n{}: {}", 141 | "urls".green(), 142 | config.urls.join(" "), 143 | "methods".blue(), 144 | config.methods.join(" "), 145 | "wordlist len".cyan(), 146 | params.len(), 147 | ); 148 | 149 | if !config.proxy.is_empty() { 150 | output += &format!("\n{}: {}", "proxy".green(), &config.proxy) 151 | } 152 | 153 | if !config.replay_proxy.is_empty() { 154 | output += &format!("\n{}: {}", "replay proxy".magenta(), &config.replay_proxy) 155 | } 156 | 157 | if config.recursion_depth != 0 { 158 | output += &format!( 159 | "\n{}: {}", 160 | "recursion depth".yellow(), 161 | &config.recursion_depth.to_string() 162 | ) 163 | } 164 | 165 | writeln!(io::stdout(), "{}\n", output).ok(); 166 | } 167 | 168 | pub fn read_urls_if_possible(filename: &str) -> Result>, io::Error> { 169 | let file = match File::open(filename) { 170 | Ok(file) => file, 171 | Err(_) => return Ok(None) 172 | }; 173 | 174 | let mut urls = Vec::new(); 175 | 176 | for url in io::BufReader::new(file).lines() { 177 | urls.push(url?); 178 | } 179 | 180 | Ok(Some(urls)) 181 | } 182 | 183 | pub(super) fn add_default_headers(curr_headers: HashMap<&str, String>) -> Vec<(String, String)> { 184 | let default_headers = [ 185 | ("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 12) AppleWebKit/601.3.9 (KHTML, like Gecko) Version/9.0.2 Firefox/99.0"), 186 | ("Accept", "*/*"), 187 | ("Accept-Encoding", "gzip, deflate") 188 | ]; 189 | 190 | let mut headers = Vec::new(); 191 | 192 | for (k, v) in default_headers { 193 | if !curr_headers.keys().any(|i| i.contains(k)) { 194 | headers.push((k.to_string(), v.to_string())) 195 | } 196 | } 197 | 198 | curr_headers.iter().map(|(k, v)| headers.push((k.to_string(), v.to_string()))).for_each(drop); 199 | 200 | headers 201 | } 202 | 203 | pub(super) fn mimic_browser_headers(curr_headers: HashMap<&str, String>) -> Vec<(String, String)> { 204 | let browser_headers = [ 205 | ("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 12) AppleWebKit/601.3.9 (KHTML, like Gecko) Version/9.0.2 Firefox/99.0"), 206 | ("Accept", "*/*"), // TODO maybe get from file extension as browsers do 207 | ("Accept-Language", "en-US;q=0.7,en;q=0.3"), 208 | ("Accept-Encoding", "gzip, deflate"), 209 | ("Dnt", "1"), 210 | ("Upgrade-Insecure-Requests", "1"), 211 | ("Sec-Fetch-Dest", "document"), 212 | ("Sec-Fetch-Mode", "navigate"), 213 | ("Sec-Fetch-Site", "same-site") 214 | ]; 215 | 216 | let mut headers = Vec::new(); 217 | 218 | for (k, v) in browser_headers { 219 | if !curr_headers.keys().any(|i| i.contains(k)) { 220 | headers.push((k.to_string(), v.to_string())) 221 | } 222 | } 223 | 224 | curr_headers.iter().map(|(k, v)| headers.push((k.to_string(), v.to_string()))).for_each(drop); 225 | 226 | headers 227 | } -------------------------------------------------------------------------------- /src/diff.rs: -------------------------------------------------------------------------------- 1 | //! Mostly taken from https://github.com/changeutils/diff-rs/blob/master/src/lib.rs 2 | 3 | use std::{collections::VecDeque, io}; 4 | 5 | pub fn diff(text1: &str, text2: &str) -> io::Result> { 6 | let mut processor = Processor::new(); 7 | { 8 | let mut replace = diffs::Replace::new(&mut processor); 9 | diffs::myers::diff( 10 | &mut replace, 11 | &text1.lines().collect::>(), 12 | &text2.lines().collect::>(), 13 | )?; 14 | } 15 | Ok(processor.result()) 16 | } 17 | 18 | struct Processor { 19 | inserted: usize, 20 | removed: usize, 21 | 22 | context: Context, 23 | result: Vec, 24 | } 25 | 26 | impl Processor { 27 | pub fn new() -> Self { 28 | Self { 29 | inserted: 0, 30 | removed: 0, 31 | 32 | context: Context::new(), 33 | result: Vec::new(), 34 | } 35 | } 36 | 37 | pub fn result(self) -> Vec { 38 | self.result 39 | } 40 | } 41 | 42 | struct Context { 43 | pub start: Option, 44 | pub data: VecDeque, 45 | pub changed: bool, 46 | 47 | pub counter: usize, 48 | pub equaled: usize, 49 | pub removed: usize, 50 | pub inserted: usize, 51 | } 52 | 53 | impl Context { 54 | pub fn new() -> Self { 55 | Self { 56 | start: None, 57 | data: VecDeque::new(), 58 | changed: false, 59 | 60 | counter: 0, 61 | equaled: 0, 62 | removed: 0, 63 | inserted: 0, 64 | } 65 | } 66 | 67 | pub fn to_vec(&self, removed: usize, inserted: usize) -> Vec { 68 | let mut start = if let Some(start) = self.start { 69 | start 70 | } else { 71 | return Vec::new(); 72 | }; 73 | if start == 0 { 74 | start = 1; 75 | } 76 | let mut data = Vec::with_capacity(self.data.len() + 1); 77 | if self.changed { 78 | data.push(format!( 79 | "-{},{} +{},{}", 80 | start, 81 | self.equaled + self.removed, 82 | start + inserted - removed, 83 | self.equaled + self.inserted, 84 | )); 85 | for s in self.data.iter() { 86 | data.push(s.to_owned()); 87 | } 88 | } 89 | data 90 | } 91 | } 92 | 93 | impl diffs::Diff for Processor { 94 | type Error = io::Error; 95 | 96 | fn equal(&mut self, old: usize, _new: usize, len: usize) -> Result<(), Self::Error> { 97 | if self.context.start.is_none() { 98 | self.context.start = Some(old); 99 | } 100 | 101 | self.context.counter = 0; 102 | for i in old..old + len { 103 | if !self.context.changed { 104 | if let Some(ref mut start) = self.context.start { 105 | *start += 1; 106 | } 107 | self.context.counter += 1; 108 | } 109 | if self.context.changed && self.context.counter == 0 && len > 0 { 110 | self.result 111 | .append(&mut self.context.to_vec(self.removed, self.inserted)); 112 | 113 | let mut context = Context::new(); 114 | 115 | context.counter = 0; 116 | context.equaled = 0; 117 | context.start = Some(i - 1); 118 | 119 | self.removed += self.context.removed; 120 | self.inserted += self.context.inserted; 121 | self.context = context; 122 | } 123 | } 124 | 125 | Ok(()) 126 | } 127 | 128 | fn delete(&mut self, old: usize, len: usize) -> Result<(), Self::Error> { 129 | if self.context.start.is_none() { 130 | self.context.start = Some(old); 131 | } 132 | 133 | self.context.changed = true; 134 | self.context.removed += len; 135 | 136 | Ok(()) 137 | } 138 | 139 | fn insert(&mut self, old: usize, _new: usize, new_len: usize) -> Result<(), Self::Error> { 140 | if self.context.start.is_none() { 141 | self.context.start = Some(old); 142 | } 143 | 144 | self.context.changed = true; 145 | self.context.inserted += new_len; 146 | 147 | Ok(()) 148 | } 149 | 150 | fn replace( 151 | &mut self, 152 | old: usize, 153 | old_len: usize, 154 | _new: usize, 155 | new_len: usize, 156 | ) -> Result<(), Self::Error> { 157 | if self.context.start.is_none() { 158 | self.context.start = Some(old); 159 | } 160 | 161 | self.context.changed = true; 162 | self.context.removed += old_len; 163 | self.context.inserted += new_len; 164 | 165 | Ok(()) 166 | } 167 | 168 | fn finish(&mut self) -> Result<(), Self::Error> { 169 | let truncation = self.context.counter; 170 | if self.context.data.len() > truncation { 171 | let new_size = self.context.data.len() - truncation; 172 | self.context.equaled -= truncation; 173 | self.context.data.truncate(new_size); 174 | } 175 | self.result 176 | .append(&mut self.context.to_vec(self.removed, self.inserted)); 177 | Ok(()) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | pub mod diff; 3 | pub mod network; 4 | pub mod runner; 5 | pub mod utils; 6 | 7 | const RANDOM_CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789"; 8 | 9 | /// To ignore pages with size > 25MB. Usually it's some binary things. Can be ignored with --force 10 | const MAX_PAGE_SIZE: usize = 25 * 1024 * 1024; 11 | 12 | const DEFAULT_PROGRESS_URL_MAX_LEN: usize = 36; 13 | 14 | /// Default random value sizes 15 | const VALUE_LENGTH: usize = 6; 16 | const RANDOM_LENGTH: usize = 5; -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate x8; 2 | use std::{ 3 | error::Error, 4 | sync::Arc, 5 | io::{self, Write}, 6 | iter::FromIterator, 7 | }; 8 | 9 | use parking_lot::Mutex; 10 | use tokio::{fs::{self, OpenOptions}, io::AsyncWriteExt}; 11 | use atty::Stream; 12 | use futures::StreamExt; 13 | use indicatif::ProgressBar; 14 | use colored::Colorize; 15 | 16 | use x8::{ 17 | config::args::get_config, 18 | config::{structs::Config, utils::write_banner_config}, 19 | network::{ 20 | request::{Request, RequestDefaults}, 21 | utils::Headers, 22 | }, 23 | runner::{ 24 | output::{ParseOutputs, RunnerOutput}, 25 | runner::Runner, 26 | utils::{Parameters, ReasonKind}, 27 | }, 28 | utils::{self, init_progress, read_lines, read_stdin_lines}, 29 | }; 30 | 31 | #[cfg(windows)] 32 | #[tokio::main] 33 | async fn main() { 34 | colored::control::set_virtual_terminal(true).unwrap(); 35 | std::process::exit(match init().await { 36 | Ok(_) => 0, 37 | Err(err) => { 38 | utils::error(err, None, None, None); 39 | 1 40 | } 41 | }); 42 | } 43 | 44 | #[cfg(not(windows))] 45 | #[tokio::main] 46 | async fn main() { 47 | std::process::exit(match init().await { 48 | Ok(_) => 0, 49 | Err(err) => { 50 | utils::error(err, None, None, None); 51 | 1 52 | } 53 | }); 54 | } 55 | 56 | /// initializes runners and passes them to run() 57 | /// also manages outputs. Probably better to rename? 58 | async fn init() -> Result<(), Box> { 59 | env_logger::init(); 60 | 61 | let config: Config = get_config()?; 62 | 63 | //if --test option is used - print request/response and quit 64 | if config.test { 65 | if config.urls.len() != 1 { 66 | Err("--test option works only with 1 url")?; 67 | } else if config.methods.len() != 1 { 68 | Err("--test option works only with 1 method")?; 69 | } 70 | 71 | //TODO move to func? 72 | writeln!( 73 | io::stdout(), 74 | "{}", 75 | Request::new_random( 76 | &RequestDefaults::from_config( 77 | &config, 78 | config.methods[0].as_str(), 79 | config.urls[0].as_str() 80 | )?, 81 | config.max.unwrap_or(16) 82 | ) 83 | .send() 84 | .await? 85 | .print_all() 86 | ) 87 | .ok(); 88 | return Ok(()); 89 | } 90 | 91 | if !config.save_responses.is_empty() { 92 | fs::create_dir_all(&config.save_responses).await?; 93 | } 94 | 95 | let mut params: Vec = Vec::new(); 96 | 97 | if !config.wordlist.is_empty() { 98 | // read parameters from a file 99 | for line in read_lines(&config.wordlist)?.flatten() { 100 | params.push(line); 101 | } 102 | // just accept piped stdin 103 | } else if !atty::is(Stream::Stdin) { 104 | // read parameters from stdin 105 | params = read_stdin_lines(); 106 | } 107 | 108 | if !config.remove_banner { 109 | write_banner_config(&config, ¶ms); 110 | } 111 | 112 | // such headers usually cause server to timeout 113 | // especially when http/2 is used 114 | // probably better to add a flag for keeping such parameters? 115 | if config.headers_discovery { 116 | params.retain(|x| "content-length" != x.to_lowercase() && "host" != x.to_lowercase()); 117 | } 118 | 119 | // -W 0 is a special option to run everything in parallel 120 | let workers = if config.workers == 0 { 121 | config.urls.len()*config.methods.len() 122 | } else { 123 | config.workers 124 | }; 125 | 126 | // open output file 127 | let mut output_file = if !config.output_file.is_empty() { 128 | let mut file = OpenOptions::new(); 129 | 130 | let file = if config.append { 131 | file.write(true).append(true) 132 | } else { 133 | file.write(true).truncate(true) 134 | }; 135 | 136 | let file = match file.open(&config.output_file).await { 137 | Ok(file) => file, 138 | Err(_) => fs::File::create(&config.output_file).await?, 139 | }; 140 | 141 | Some(file) 142 | } else { 143 | None 144 | }; 145 | 146 | let shared_output_file = Arc::new(Mutex::new(&mut output_file)); 147 | 148 | let runner_outputs = 149 | futures::stream::iter(init_progress(&config).iter().enumerate().skip(1).map( 150 | |(id, (progress_bar, url_set))| { 151 | 152 | let shared_output_file = Arc::clone(&shared_output_file); 153 | 154 | // each url set should have each own list of parameters 155 | let params = params.clone(); 156 | 157 | // each url set should have it's own immutable pointer to config 158 | let config = &config; 159 | 160 | //let output_file = output_file.as_ref().unwrap().try_clone(); 161 | 162 | async move { 163 | let mut runner_outputs = Vec::new(); 164 | 165 | // for now url set are used only in case --one-worker-per-host option is provided 166 | // otherwise it's just url sets of 1 url 167 | for url in url_set { 168 | for method in &config.methods.clone() { 169 | // each method should have each own list of parameters (we're changing this list through the run) 170 | let mut params = params.clone(); 171 | 172 | let mut request_defaults = match RequestDefaults::from_config( 173 | config, 174 | method.as_str(), 175 | url.as_str(), 176 | ) { 177 | Ok(val) => val, 178 | Err(err) => { 179 | utils::error(err, Some(url), Some(progress_bar), Some(config)); 180 | continue; 181 | } 182 | }; 183 | 184 | // get cookies 185 | if let Err(err) = 186 | Request::new(&request_defaults, Vec::new()).send().await 187 | { 188 | utils::error(err, Some(url), Some(progress_bar), Some(config)); 189 | continue; 190 | }; 191 | 192 | match run( 193 | config, 194 | &mut request_defaults, 195 | &mut params, 196 | &progress_bar, 197 | id, 198 | ) 199 | .await 200 | { 201 | Ok(val) => { 202 | // if output format is not json we can print output and write to file in real time 203 | if config.output_format != "json" { 204 | let mut output_file = shared_output_file.lock(); 205 | let output = val.parse(config); 206 | 207 | if output_file.is_some() && !(config.remove_empty && val.found_params.is_empty()) { 208 | 209 | match output_file.as_mut().unwrap().write_all( 210 | &strip_ansi_escapes::strip(&(output.normal().clear().to_string()+"\n").as_bytes()).unwrap() 211 | ).await { 212 | Ok(()) => output_file.as_mut().unwrap().flush().await.unwrap(), 213 | Err(err) => utils::error(err, Some(url), Some(progress_bar), Some(config)), 214 | }; 215 | } 216 | 217 | let msg = if config.verbose > 0 { 218 | format!("\n{}\n\n", output) 219 | } else { 220 | format!("{}", output) 221 | }; 222 | 223 | if config.disable_progress_bar { 224 | writeln!(io::stdout(), "{}", msg).ok(); 225 | } else { 226 | progress_bar.println(msg); 227 | } 228 | 229 | } else { 230 | runner_outputs.push(val) 231 | } 232 | }, 233 | Err(err) => { 234 | utils::error(err, Some(url), Some(progress_bar), Some(config)) 235 | } 236 | } 237 | } 238 | } 239 | runner_outputs 240 | } 241 | }, 242 | )) 243 | .buffer_unordered(workers) 244 | .collect::>>() 245 | .await; 246 | 247 | // works only in case json output is used. 248 | // otherwise runner_outputs is an empty vector 249 | // and all the printing work is done within the futures above 250 | if !runner_outputs.is_empty() { 251 | let output = runner_outputs 252 | .into_iter() 253 | .flatten() 254 | .filter(|x| !(config.remove_empty && x.found_params.is_empty())) 255 | .collect::>() 256 | .parse_output(&config); 257 | 258 | if output_file.is_some() { 259 | output_file.as_mut().unwrap().write_all(output.as_bytes()).await?; 260 | output_file.as_mut().unwrap().flush().await?; 261 | } 262 | 263 | write!(io::stdout(), "\n{}", output).ok(); 264 | } 265 | 266 | Ok(()) 267 | } 268 | 269 | async fn run( 270 | config: &Config, 271 | request_defaults: &mut RequestDefaults, 272 | params: &mut Vec, 273 | progress_bar: &ProgressBar, 274 | id: usize, 275 | ) -> Result> { 276 | let mut runner_output = Runner::new(config, request_defaults, progress_bar, id) 277 | .await? 278 | .run(params) 279 | .await?; 280 | 281 | // the whole block related to the recursive searching 282 | if !runner_output.found_params.is_empty() { 283 | for depth in 1..config.recursion_depth + 1 { 284 | // remove already found parameters from the list to prevent duplicates 285 | params.retain(|x| !runner_output.found_params.contains_name(x)); 286 | 287 | // custom parameters work badly with recursion enabled 288 | request_defaults.disable_custom_parameters = true; 289 | 290 | // so we are keeping parameters that don't change pages' code 291 | // or change it to 200 292 | // we cant simply overwrite request_defaults.parameters because there's user-supplied parameters as well. 293 | request_defaults.parameters.append(&mut Vec::from_iter( 294 | runner_output 295 | .found_params 296 | .iter() 297 | .filter(|x| { 298 | !request_defaults.parameters.contains_key(&x.name) 299 | && (x.reason_kind != ReasonKind::Code || x.status == 200) 300 | }) 301 | .map(|x| (x.get())), 302 | )); 303 | 304 | utils::info( 305 | config, 306 | id, 307 | progress_bar, 308 | "recursion", 309 | format!( 310 | "({}) repeating with {}", 311 | depth, 312 | request_defaults 313 | .parameters 314 | .iter() 315 | .map(|x| x.0.as_str()) 316 | .collect::>() 317 | .join(", ") 318 | ), 319 | ); 320 | 321 | let mut new_found_params = Runner::new(config, request_defaults, progress_bar, id) 322 | .await? 323 | .run(params) 324 | .await? 325 | .found_params; 326 | 327 | // no new params where found - just quit the loop 328 | if !new_found_params 329 | .iter() 330 | .any(|x| !runner_output.found_params.contains_name(&x.name)) 331 | { 332 | break; 333 | } 334 | 335 | runner_output.found_params.append(&mut new_found_params); 336 | } 337 | } 338 | 339 | // we probably changed request_defaults.parameters within the loop above 340 | // so we are removing all of the added parameters in there 341 | // leaving only user-supplied ones 342 | // (to not cause double parameters in some output types) 343 | request_defaults.parameters = request_defaults 344 | .parameters 345 | .iter() 346 | .filter(|x| !runner_output.found_params.contains_name(&x.0)) 347 | .map(|x| x.to_owned()) 348 | .collect(); 349 | 350 | runner_output.prepare(config, request_defaults); 351 | 352 | Ok(runner_output) 353 | } 354 | -------------------------------------------------------------------------------- /src/network/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod request; 2 | pub mod response; 3 | pub mod utils; 4 | 5 | mod tests; 6 | -------------------------------------------------------------------------------- /src/network/request.rs: -------------------------------------------------------------------------------- 1 | use crate::{config::structs::Config, utils::random_line, VALUE_LENGTH, RANDOM_LENGTH}; 2 | use itertools::Itertools; 3 | use lazy_static::lazy_static; 4 | use percent_encoding::utf8_percent_encode; 5 | use regex::Regex; 6 | use reqwest::Client; 7 | use std::{ 8 | collections::HashMap, 9 | convert::TryFrom, 10 | error::Error, 11 | iter::FromIterator, 12 | time::{Duration, Instant}, 13 | }; 14 | use url::Url; 15 | 16 | /// in order to be able to use make_query() for headers as well 17 | const HEADERS_TEMPLATE: &str = "%k\x00@%=%@\x00%v"; 18 | const HEADERS_MIDDLE: &str = "\x00@%=%@\x00"; 19 | const HEADERS_JOINER: &str = "\x01@%&%@\x01"; 20 | 21 | use super::{ 22 | response::Response, 23 | utils::{DataType, Headers, InjectionPlace, FRAGMENT, create_client, is_binary_content}, 24 | }; 25 | 26 | #[derive(Debug, Clone, Default)] 27 | pub struct RequestDefaults { 28 | /// default request data 29 | pub method: String, 30 | pub scheme: String, 31 | pub path: String, 32 | pub host: String, 33 | pub port: u16, 34 | 35 | /// custom user supplied headers or default ones 36 | pub custom_headers: Vec<(String, String)>, 37 | 38 | /// how much to sleep between requests in millisecs 39 | pub delay: Duration, //MOVE to config 40 | 41 | /// default reqwest client 42 | pub client: Client, 43 | 44 | /// parameter template, for example %k=%v 45 | pub template: String, 46 | 47 | /// how to join parameters, for example '&' 48 | pub joiner: String, 49 | 50 | /// whether to encode the query like param1=value1¶m2=value2 -> param1%3dvalue1%26param2%3dvalue2 51 | pub encode: bool, 52 | 53 | /// to replace {"key": "false"} with {"key": false} 54 | pub is_json: bool, 55 | 56 | /// default body 57 | pub body: String, 58 | 59 | /// whether to include parameters like debug=true to the list 60 | pub disable_custom_parameters: bool, 61 | 62 | /// parameters to add to every request 63 | /// it is used in recursion search 64 | pub parameters: Vec<(String, String)>, 65 | 66 | /// where the injection point is 67 | pub injection_place: InjectionPlace, 68 | 69 | /// the default amount of reflection per non existing parameter 70 | pub amount_of_reflections: usize, 71 | 72 | /// check body of responses with binary content type 73 | pub check_binary: bool, 74 | } 75 | 76 | #[derive(Debug, Clone)] 77 | pub struct Request<'a> { 78 | pub defaults: &'a RequestDefaults, 79 | 80 | /// vector of supplied parameters 81 | pub parameters: Vec, 82 | 83 | /// parsed parameters (key, value) 84 | pub prepared_parameters: Vec<(String, String)>, 85 | 86 | /// parameters with not random values 87 | /// we need this vector to ignore searching for reflections for these parameters 88 | /// for example admin=1 - its obvious that 1 can be reflected unpredictable amount of times 89 | pub non_random_parameters: Vec<(String, String)>, 90 | 91 | pub headers: Vec<(String, String)>, 92 | 93 | pub body: String, 94 | 95 | /// we can't use defaults.path because there can be {{random}} variable that need to be replaced 96 | pub path: String, 97 | 98 | /// whether the request was prepared 99 | /// {{random}} things replaced, prepared_parameters filled 100 | pub prepared: bool, 101 | } 102 | 103 | impl<'a> Request<'a> { 104 | pub fn new(l: &'a RequestDefaults, parameters: Vec) -> Self { 105 | Self { 106 | path: l.path.to_owned(), 107 | defaults: l, 108 | headers: Vec::new(), 109 | body: l.body.clone(), 110 | parameters, 111 | prepared_parameters: Vec::new(), //l.parameters.clone(), 112 | non_random_parameters: Vec::new(), 113 | prepared: false, 114 | } 115 | } 116 | 117 | pub fn new_random(l: &'a RequestDefaults, max: usize) -> Self { 118 | let parameters = Vec::from_iter((0..max).map(|_| random_line(VALUE_LENGTH))); 119 | Request::new(l, parameters) 120 | } 121 | 122 | pub fn set_header>(&mut self, key: S, value: S) { 123 | self.headers.push((key.into(), value.into())); 124 | } 125 | 126 | pub fn set_headers(&mut self, headers: Vec<(String, String)>) { 127 | for (k, v) in headers { 128 | self.headers.push((k, v)); 129 | } 130 | } 131 | 132 | pub fn url(&self) -> String { 133 | format!( 134 | "{}://{}:{}{}", 135 | &self.defaults.scheme, &self.defaults.host, &self.defaults.port, &self.path 136 | ) 137 | } 138 | 139 | pub fn make_query(&self) -> String { 140 | lazy_static! { 141 | static ref RE_JSON_WORDS_WITHOUT_QUOTES: Regex = 142 | Regex::new(r#"^([1-9]\d*|null|false|true)$"#).unwrap(); 143 | } 144 | 145 | let query = if self.defaults.is_json { 146 | self.prepared_parameters 147 | .iter() 148 | .chain(self.defaults.parameters.iter()) 149 | // not very optimal because we know that there's a lot of random parameters 150 | // that doesn't need to be checked 151 | .map(|(k, v)| { 152 | if RE_JSON_WORDS_WITHOUT_QUOTES.is_match(v) { 153 | self.defaults.template.replace("%k", k).replace("%v", v) 154 | } else { 155 | self.defaults 156 | .template 157 | .replace("%k", k) 158 | .replace("%v", &format!("\"{}\"", v)) 159 | } 160 | }) 161 | .collect::>() 162 | .join(&self.defaults.joiner) 163 | } else { 164 | self.prepared_parameters 165 | .iter() 166 | .chain(self.defaults.parameters.iter()) 167 | .map(|(k, v)| self.defaults.template.replace("%k", k).replace("%v", v)) 168 | .collect::>() 169 | .join(&self.defaults.joiner) 170 | }; 171 | 172 | if self.defaults.encode { 173 | utf8_percent_encode(&query, &FRAGMENT).to_string() 174 | } else { 175 | query 176 | } 177 | } 178 | 179 | /// replace injection points with parameters 180 | /// replace templates ({{random}}) with random values 181 | /// additional param is for reflection counting TODO REMOVE 182 | /// 183 | /// in case self.parameters contains parameter with "=" 184 | /// it gets splitted by = and the default random value gets replaced with the right part: 185 | /// admin=true -> (admin, true) vs admin -> (admin, df32w) 186 | pub fn prepare(&mut self) { 187 | if self.prepared { 188 | return; 189 | } 190 | self.prepared = true; 191 | 192 | self.non_random_parameters = Vec::from_iter( 193 | self.parameters 194 | .iter() 195 | .filter(|x| x.contains('=')) 196 | .map(|x| x.split('=')) 197 | .map(|mut x| { 198 | ( 199 | x.next().unwrap().to_owned(), 200 | x.next().unwrap_or("").to_owned(), 201 | ) 202 | }), 203 | ); 204 | 205 | self.prepared_parameters = Vec::from_iter( 206 | // append self.prepared_parameters (can be set from RequestDefaults using recursive search) 207 | self.prepared_parameters 208 | .iter() 209 | .map(|(k, v)| (k.to_owned(), v.to_owned())) 210 | // append parameters with not random values 211 | .chain( 212 | self.non_random_parameters 213 | .iter() 214 | .map(|(k, v)| (k.to_owned(), v.to_owned())), 215 | ) 216 | // append random parameters 217 | .chain( 218 | self.parameters 219 | .iter() 220 | .filter(|x| !x.is_empty() && !x.contains("=")) 221 | .map(|x| (x.to_owned(), random_line(VALUE_LENGTH))), 222 | ), 223 | ); 224 | 225 | if self.defaults.injection_place != InjectionPlace::HeaderValue { 226 | for (k, v) in self.defaults.custom_headers.iter() { 227 | self.set_header(k, &v.replace("{{random}}", &random_line(RANDOM_LENGTH))); 228 | } 229 | } 230 | self.path = self.path.replace("{{random}}", &random_line(RANDOM_LENGTH)); 231 | self.body = self.body.replace("{{random}}", &random_line(RANDOM_LENGTH)); 232 | 233 | match self.defaults.injection_place { 234 | InjectionPlace::Path => self.path = self.path.replace("%s", &self.make_query()), 235 | InjectionPlace::Body => { 236 | self.body = self.body.replace("%s", &self.make_query()); 237 | 238 | if !self.defaults.custom_headers.contains_key("Content-Type") { 239 | if self.defaults.is_json { 240 | self.set_header("Content-Type", "application/json"); 241 | } else { 242 | self.set_header("Content-Type", "application/x-www-form-urlencoded"); 243 | } 244 | } 245 | } 246 | InjectionPlace::HeaderValue => { 247 | // in case someone searches headers while sending a valid body - it's usually important to set Content-Type header as well. 248 | if !self.defaults.custom_headers.contains_key("Content-Type") && self.defaults.method != "GET" && self.defaults.method != "HEAD" && !self.body.is_empty() { 249 | if self.body.starts_with('{') { 250 | self.set_header("Content-Type", "application/json"); 251 | } else { 252 | self.set_header("Content-Type", "application/x-www-form-urlencoded"); 253 | } 254 | } 255 | 256 | for (k, v) in self.defaults.custom_headers.iter() { 257 | self.set_header( 258 | k, 259 | &v.replace("{{random}}", &random_line(RANDOM_LENGTH)) 260 | .replace("%s", &self.make_query()), 261 | ); 262 | } 263 | } 264 | InjectionPlace::Headers => { 265 | // in case someone searches headers while sending a valid body - it's usually important to set Content-Type header as well. 266 | if !self.defaults.custom_headers.contains_key("Content-Type") && self.defaults.method != "GET" && self.defaults.method != "HEAD" && !self.body.is_empty() { 267 | if self.body.starts_with('{') { 268 | self.set_header("Content-Type", "application/json"); 269 | } else { 270 | self.set_header("Content-Type", "application/x-www-form-urlencoded"); 271 | } 272 | } 273 | 274 | let headers: Vec<(String, String)> = self 275 | .make_query() 276 | .split(&self.defaults.joiner) 277 | .filter(|x| !x.is_empty()) 278 | .map(|x| x.split(HEADERS_MIDDLE)) 279 | .map(|mut x| (x.next().unwrap().to_owned(), x.next().unwrap().to_owned())) 280 | .collect(); 281 | 282 | self.set_headers(headers); 283 | } 284 | } 285 | } 286 | 287 | pub async fn send_by(self, clients: &Client) -> Result, Box> { 288 | match self.clone().request(clients).await { 289 | Ok(val) => Ok(val), 290 | Err(_) => { 291 | tokio::time::sleep(Duration::from_secs(10)).await; 292 | Ok(self.clone().request(clients).await?) 293 | } 294 | } 295 | } 296 | 297 | // we need to somehow impl Send and Sync for error (for using send() within async recursive func) 298 | // therefore we are wrapping the original call to send() 299 | // not a good way tho, maybe someone can suggest a better one 300 | pub async fn wrapped_send(self) -> Result, Box> { 301 | match self.send().await { 302 | Err(err) => Err(err.to_string().into()), 303 | Ok(val) => Ok(val), 304 | } 305 | } 306 | 307 | pub async fn send(self) -> Result, Box> { 308 | let dc = &self.defaults.client; 309 | self.send_by(dc).await 310 | } 311 | 312 | async fn request(mut self, client: &Client) -> Result, reqwest::Error> { 313 | self.prepare(); 314 | 315 | let mut request = http::Request::builder() 316 | .method(self.defaults.method.as_str()) 317 | .uri(self.url()); 318 | 319 | for (k, v) in &self.headers { 320 | request = request.header(k, v) 321 | } 322 | 323 | let request = request.body(self.body.to_owned()).unwrap(); 324 | 325 | tokio::time::sleep(self.defaults.delay).await; 326 | 327 | let reqwest_req = reqwest::Request::try_from(request).unwrap(); 328 | 329 | let start = Instant::now(); 330 | 331 | let res = client.execute(reqwest_req).await?; 332 | 333 | let duration = start.elapsed(); 334 | 335 | let mut headers: Vec<(String, String)> = Vec::new(); 336 | 337 | for (k, v) in res.headers() { 338 | let k = k.to_string(); 339 | 340 | // sometimes conversion may fail 341 | let v = match v.to_str() { 342 | Ok(val) => val, 343 | Err(_) => { 344 | log::debug!("Unable to parse {} header. The value is {:?}", k, v); 345 | "" 346 | } 347 | }.to_string(); 348 | 349 | headers.push((k, v)); 350 | } 351 | 352 | let code = res.status().as_u16(); 353 | let http_version = Some(res.version()); 354 | 355 | let body_bytes = res.bytes().await?.to_vec(); 356 | 357 | let text = if is_binary_content(headers.get_value_case_insensitive("content-type")) && !self.defaults.check_binary { 358 | String::new() 359 | } else { 360 | String::from_utf8_lossy(&body_bytes).to_string() 361 | }; 362 | 363 | let mut response = Response { 364 | code, 365 | headers, 366 | time: duration.as_millis(), 367 | text, 368 | request: Some(self), 369 | reflected_parameters: HashMap::new(), 370 | http_version, 371 | }; 372 | 373 | response.beautify_body(); 374 | response.add_headers(); 375 | 376 | Ok(response) 377 | } 378 | 379 | /// the function is used when there was a error during the request 380 | pub fn empty_response(mut self) -> Response<'a> { 381 | self.prepare(); 382 | Response { 383 | time: 0, 384 | code: 0, 385 | headers: Vec::new(), 386 | text: String::new(), 387 | reflected_parameters: HashMap::new(), 388 | request: Some(self), 389 | http_version: None, 390 | } 391 | } 392 | 393 | pub fn print(&mut self) -> String { 394 | self.prepare(); 395 | self.print_sent() 396 | } 397 | 398 | pub fn print_sent(&self) -> String { 399 | let host = if self.headers.contains_key("Host") { 400 | self.headers.get_value("Host").unwrap() 401 | } else { 402 | self.defaults.host.to_owned() 403 | }; 404 | 405 | let mut str_req = format!( 406 | "{} {} HTTP/1.1\nHost: {}\n", 407 | &self.defaults.method, self.path, host 408 | ); 409 | 410 | for (k, v) in self.headers.iter().sorted() { 411 | if k != "Host" { 412 | str_req += &format!("{}: {}\n", k, v) 413 | } 414 | } 415 | 416 | str_req += &format!("\n{}", self.body); 417 | 418 | str_req 419 | } 420 | } 421 | 422 | impl<'a> RequestDefaults { 423 | pub fn from_config>( 424 | config: &Config, 425 | method: S, 426 | url: S, 427 | ) -> Result> { 428 | Self::new( 429 | method.into().as_str(), //method needs to be set explicitly via .set_method() 430 | url.into().as_str(), //as well as url 431 | config.custom_headers.clone(), 432 | config.delay, 433 | create_client(config, false)?, 434 | config.template.clone(), 435 | config.joiner.clone(), 436 | config.encode, 437 | config.data_type.clone(), 438 | config.invert, 439 | config.headers_discovery, 440 | &config.body, 441 | config.disable_custom_parameters, 442 | config.check_binary 443 | ) 444 | } 445 | 446 | pub fn new + From + std::fmt::Debug>( 447 | method: &str, 448 | url: &str, 449 | custom_headers: Vec<(String, String)>, 450 | delay: Duration, 451 | client: Client, 452 | template: Option, 453 | joiner: Option, 454 | encode: bool, 455 | mut data_type: Option, 456 | invert: bool, 457 | headers_discovery: bool, 458 | body: &str, 459 | disable_custom_parameters: bool, 460 | check_binary: bool, 461 | ) -> Result> { 462 | 463 | let mut injection_place = if headers_discovery { 464 | InjectionPlace::Headers 465 | } else if (method == "POST" || method == "PUT" || method == "PATCH" || method == "DELETE") && !invert 466 | || (method != "POST" && method != "PUT" && method != "PATCH" && method != "DELETE" && invert) { 467 | InjectionPlace::Body 468 | } else { 469 | InjectionPlace::Path 470 | }; 471 | 472 | if headers_discovery { 473 | data_type = Some(DataType::Headers); 474 | 475 | if custom_headers.iter().any(|x| x.1.contains("%s")) { 476 | injection_place = InjectionPlace::HeaderValue; 477 | } 478 | } 479 | 480 | let data_type = if data_type != Some(DataType::ProbablyJson) { 481 | data_type 482 | 483 | // explained in DataType enum comments 484 | // tl.dr. data_type was taken from a parsed request's content-type so we are not 100% sure what did a user mean 485 | // we don't need probablyurlencoded because urlencoded is fine for get requests 486 | } else if injection_place == InjectionPlace::Body && data_type == Some(DataType::ProbablyJson) { 487 | Some(DataType::Json) 488 | } else if injection_place == InjectionPlace::Path { 489 | Some(DataType::Urlencoded) 490 | } else { 491 | unreachable!() 492 | }; 493 | 494 | let (guessed_template, guessed_joiner, is_json, data_type) = 495 | RequestDefaults::guess_data_format(body, &injection_place, data_type); 496 | 497 | let (template, joiner) = ( 498 | template 499 | .unwrap_or_else(|| guessed_template.to_string().into()) 500 | .into(), 501 | joiner.unwrap_or_else(|| guessed_joiner.to_string().into()).into().replace("\\r", "\r").replace("\\n", "\n"), 502 | ); 503 | 504 | let url = Url::parse(url)?; 505 | 506 | let (path, body) = if let Some(data_type) = data_type { 507 | RequestDefaults::fix_path_and_body( 508 | // &url[url::Position::BeforePath..].to_string() instead of url.path() because we need to preserve query as well 509 | &url[url::Position::BeforePath..], 510 | body, 511 | &joiner, 512 | &injection_place, 513 | data_type, 514 | ) 515 | } else { 516 | // injection within headers 517 | ( 518 | url[url::Position::BeforePath..].to_string(), 519 | body.to_owned(), 520 | ) 521 | }; 522 | 523 | Ok(Self { 524 | method: method.to_string(), 525 | scheme: url.scheme().to_string(), 526 | path, 527 | host: url.host().ok_or("Host missing")?.to_string(), 528 | custom_headers, 529 | port: url.port_or_known_default().ok_or("Wrong scheme")?, 530 | delay, 531 | client, 532 | template, 533 | joiner, 534 | encode, 535 | is_json, 536 | body, 537 | disable_custom_parameters, 538 | injection_place, 539 | 540 | amount_of_reflections: 0, 541 | 542 | parameters: Vec::new(), 543 | 544 | check_binary 545 | }) 546 | } 547 | 548 | /// returns template, joiner, whether the data is json, DataType if the injection point isn't within headers 549 | fn guess_data_format( 550 | body: &str, 551 | injection_place: &InjectionPlace, 552 | data_type: Option, 553 | ) -> (&'a str, &'a str, bool, Option) { 554 | if data_type.is_some() && data_type != Some(DataType::Headers) { 555 | match data_type { 556 | // %v isn't within quotes because not every json value needs to be in quotes 557 | Some(DataType::Json) => ("\"%k\":%v", ",", true, Some(DataType::Json)), 558 | Some(DataType::Urlencoded) => ("%k=%v", "&", false, Some(DataType::Urlencoded)), 559 | _ => unreachable!(), 560 | } 561 | } else { 562 | match injection_place { 563 | InjectionPlace::Body => { 564 | if body.starts_with('{') { 565 | ("\"%k\":%v", ",", true, Some(DataType::Json)) 566 | } else { 567 | ("%k=%v", "&", false, Some(DataType::Urlencoded)) 568 | } 569 | } 570 | InjectionPlace::HeaderValue => ("%k=%v", ";", false, None), 571 | InjectionPlace::Path => ("%k=%v", "&", false, Some(DataType::Urlencoded)), 572 | InjectionPlace::Headers => (HEADERS_TEMPLATE, HEADERS_JOINER, false, None), 573 | } 574 | } 575 | } 576 | 577 | /// adds injection points where necessary 578 | fn fix_path_and_body( 579 | path: &str, 580 | body: &str, 581 | joiner: &str, 582 | injection_place: &InjectionPlace, 583 | data_type: DataType, 584 | ) -> (String, String) { 585 | match injection_place { 586 | InjectionPlace::Body => { 587 | if body.contains("%s") { 588 | (path.to_string(), body.to_string()) 589 | } else if body.is_empty() { 590 | match data_type { 591 | DataType::Urlencoded => (path.to_string(), "%s".to_string()), 592 | DataType::Json => (path.to_string(), "{%s}".to_string()), 593 | _ => unreachable!(), 594 | } 595 | } else { 596 | match data_type { 597 | DataType::Urlencoded => (path.to_string(), format!("{}{}%s", body, joiner)), 598 | DataType::Json => { 599 | let mut body = body.to_owned(); 600 | body.pop(); // remove the last '}' 601 | if body != "{" { 602 | (path.to_string(), format!("{},%s}}", body)) 603 | } else { 604 | // the json body was empty so the first comma is not needed 605 | (path.to_string(), format!("{}%s}}", body)) 606 | } 607 | } 608 | _ => unreachable!(), 609 | } 610 | } 611 | } 612 | InjectionPlace::Path => { 613 | if path.contains("%s") { 614 | (path.to_string(), body.to_string()) 615 | } else if path.contains('?') { 616 | (format!("{}{}%s", path, joiner), body.to_string()) 617 | } else if joiner == "&" { 618 | (format!("{}?%s", path), body.to_string()) 619 | } else { 620 | // some very non-standart configuration 621 | (format!("{}%s", path), body.to_string()) 622 | } 623 | } 624 | _ => (path.to_string(), body.to_string()), 625 | } 626 | } 627 | 628 | /// recreates url 629 | pub fn url(&self) -> String { 630 | format!("{}://{}:{}{}", self.scheme, self.host, self.port, self.path) 631 | } 632 | 633 | /// recreates url without default port 634 | pub fn url_without_default_port(&self) -> String { 635 | let port = if self.port == 443 || self.port == 80 { 636 | String::new() 637 | } else { 638 | format!(":{}", self.port) 639 | }; 640 | 641 | format!("{}://{}{}{}", self.scheme, self.host, port, self.path) 642 | } 643 | } 644 | -------------------------------------------------------------------------------- /src/network/response.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, error::Error, iter::FromIterator, io::{self, Write}}; 2 | 3 | use colored::Colorize; 4 | use indicatif::ProgressBar; 5 | use itertools::Itertools; 6 | use lazy_static::lazy_static; 7 | use regex::Regex; 8 | 9 | use crate::{config::structs::Config, diff::diff, runner::utils::ReasonKind, utils::{color_id, is_id_important}}; 10 | 11 | use super::{ 12 | request::Request, 13 | utils::{save_request, Headers}, 14 | }; 15 | 16 | #[derive(Debug, Clone, Default)] 17 | pub struct Response<'a> { 18 | /// time from the sent request to response headers 19 | pub time: u128, 20 | 21 | /// response's status code 22 | pub code: u16, 23 | 24 | /// headers with order preserved 25 | pub headers: Vec<(String, String)>, 26 | 27 | /// headers + body 28 | pub text: String, 29 | 30 | /// hashmap that fills later with possible reflected parameters 31 | pub reflected_parameters: HashMap, 32 | 33 | /// the sent request struct itself 34 | /// None only in initial_request due to lifetime issues 35 | pub request: Option>, 36 | 37 | /// None only when the request failed 38 | pub http_version: Option, 39 | } 40 | 41 | //Owo 42 | unsafe impl Send for Response<'_> {} 43 | 44 | /// helps manage response codes 45 | #[derive(PartialEq, Eq)] 46 | pub enum Status { 47 | Ok, //2xx 48 | Redirect, //3xx 49 | UserFault, //4xx 50 | ServerFault, //5xx 51 | Other, 52 | } 53 | 54 | impl<'a> Response<'a> { 55 | /// count how many times we can see the string in the response 56 | pub fn count(&self, string: &str) -> usize { 57 | let re = Regex::new(&format!("(?i){}", string)).unwrap(); 58 | re.find_iter(&self.text).count() 59 | } 60 | 61 | /// calls check_diffs & returns code and found diffs 62 | pub fn compare( 63 | &self, 64 | initial_response: &'a Response<'a>, 65 | old_diffs: &[String], 66 | ) -> Result<(bool, Vec), Box> { 67 | let mut is_code_diff: bool = false; 68 | let mut diffs: Vec = Vec::new(); 69 | 70 | if initial_response.code != self.code { 71 | is_code_diff = true 72 | } 73 | 74 | // just push every found diff to the vector of diffs 75 | for diff in diff(&self.print(), &initial_response.print())? { 76 | if !diffs.contains(&diff) && !old_diffs.contains(&diff) { 77 | diffs.push(diff); 78 | // sometimes returns a few same diffs. They should be considered as well 79 | } else if !old_diffs.contains(&diff) { 80 | let mut c = 1; 81 | while diffs.contains(&format!("{} ({})", &diff, c)) { 82 | c += 1 83 | } 84 | diffs.push(format!("{} ({})", &diff, c)); 85 | } 86 | } 87 | 88 | diffs.sort(); 89 | 90 | Ok((is_code_diff, diffs)) 91 | } 92 | 93 | /// adds new lines where necessary in order to increase accuracy in diffing 94 | pub fn beautify_body(&mut self) { 95 | lazy_static! { 96 | static ref RE_JSON_WORDS_WITHOUT_QUOTES: Regex = 97 | Regex::new(r#"^(\d+|null|false|true)$"#).unwrap(); 98 | static ref RE_JSON_BRACKETS: Regex = 99 | Regex::new(r#"(?P(\{"|"\}|\[("|\d)|("|\d)\]))"#).unwrap(); 100 | static ref RE_JSON_COMMA_AFTER_DIGIT: Regex = 101 | Regex::new(r#"(?P"[\w\.-]*"):(?P\d+),"#).unwrap(); 102 | static ref RE_JSON_COMMA_AFTER_BOOL: Regex = 103 | Regex::new(r#"(?P"[\w\.-]*"):(?P(false|null|true)),"#).unwrap(); 104 | } 105 | 106 | self.text = if (self.headers.contains_key("content-type") 107 | && self 108 | .headers 109 | .get_value_case_insensitive("content-type") 110 | .unwrap() 111 | .contains("json")) 112 | || (self.text.starts_with('{') && self.text.ends_with('}')) 113 | { 114 | let body = self.text.replace("\\\"", "'").replace("\",", "\",\n"); 115 | let body = RE_JSON_BRACKETS.replace_all(&body, "${bracket}\n"); 116 | let body = RE_JSON_COMMA_AFTER_DIGIT.replace_all(&body, "$first:$second,\n"); 117 | let body = RE_JSON_COMMA_AFTER_BOOL.replace_all(&body, "$first:$second,\n"); 118 | 119 | body.to_string() 120 | } else { 121 | self.text.replace('>', ">\n") 122 | } 123 | } 124 | 125 | /// finds parameters with the different amount of reflections and adds them to self.reflected_parameters 126 | pub fn fill_reflected_parameters(&mut self, initial_response: &Response) { 127 | // remove non random parameters from prepared parameters because they would cause false positives in this check 128 | let prepated_parameters: Vec<&(String, String)> = if !self 129 | .request 130 | .as_ref() 131 | .unwrap() 132 | .non_random_parameters 133 | .is_empty() 134 | { 135 | Vec::from_iter( 136 | self.request 137 | .as_ref() 138 | .unwrap() 139 | .prepared_parameters 140 | .iter() 141 | .filter(|x| { 142 | !self 143 | .request 144 | .as_ref() 145 | .unwrap() 146 | .non_random_parameters 147 | .contains_key(&x.0) 148 | }), 149 | ) 150 | } else { 151 | Vec::from_iter(self.request.as_ref().unwrap().prepared_parameters.iter()) 152 | }; 153 | 154 | for (k, v) in prepated_parameters.iter() { 155 | // maybe it's better to remove count from the initial response 156 | // sure it's increases accuracy a bit, but the performance impact is high 157 | let new_count = self.count(v) - initial_response.count(v); 158 | 159 | if self 160 | .request 161 | .as_ref() 162 | .unwrap() 163 | .defaults 164 | .amount_of_reflections 165 | != new_count 166 | { 167 | self.reflected_parameters.insert(k.to_string(), new_count); 168 | } 169 | } 170 | } 171 | 172 | /// returns parameters with different amount of reflections and tells whether we need to recheck the remaining parameters 173 | pub fn proceed_reflected_parameters(&self) -> (Option<&str>, bool) { 174 | if self.reflected_parameters.is_empty() { 175 | return (None, false); 176 | 177 | // only one reflected parameter - return it 178 | // but firstly we need to check that the amount of parameters is > 2 179 | // because there can be a parameter that changes the page 180 | // in this case, the page may return the different amount of reflections to every parameter 181 | // and this another random parameter will look like a reflected one and may cause false positives 182 | } else if self.reflected_parameters.len() == 1 && self.request.as_ref().unwrap().prepared_parameters.len() > 2 { 183 | return ( 184 | Some(self.reflected_parameters.keys().next().unwrap()), 185 | false, 186 | ); 187 | }; 188 | 189 | // only one reflected parameter besides additional one - return it 190 | if self.request.as_ref().unwrap().prepared_parameters.len() 191 | == self.reflected_parameters.len() 192 | && self.reflected_parameters.len() == 1 193 | { 194 | return ( 195 | Some(self.reflected_parameters.keys().next().unwrap()), 196 | false, 197 | ); 198 | } 199 | 200 | // save parameters by their amount of reflections 201 | let mut parameters_by_reflections: HashMap> = HashMap::new(); 202 | 203 | for (k, v) in self.reflected_parameters.iter() { 204 | if parameters_by_reflections.contains_key(v) { 205 | parameters_by_reflections.get_mut(v).unwrap().push(k); 206 | } else { 207 | parameters_by_reflections.insert(*v, vec![k]); 208 | } 209 | } 210 | 211 | // try to find a parameter with different amount of reflections between all of them 212 | if parameters_by_reflections.len() == 2 { 213 | for (_, v) in parameters_by_reflections.iter() { 214 | if v.len() == 1 { 215 | return (Some(v[0]), true); 216 | } 217 | } 218 | } 219 | 220 | // the reflections weren't stable. It's better to recheck the parameters 221 | (None, true) 222 | } 223 | 224 | /// adds headers to response text 225 | pub fn add_headers(&mut self) { 226 | let mut text = String::new(); 227 | for (k, v) in self.headers.iter().sorted() { 228 | text += &format!("{}: {}\n", k, v); 229 | } 230 | 231 | self.text = text + "\n" + &self.text; 232 | } 233 | 234 | /// write about found parameter to stdout and save when needed 235 | pub fn write_and_save( 236 | &self, 237 | id: usize, 238 | config: &Config, 239 | initial_response: &Response, 240 | reason_kind: ReasonKind, 241 | parameter: &str, 242 | diff: Option<&str>, 243 | progress_bar: &ProgressBar, 244 | ) -> Result<(), Box> { 245 | 246 | let id_if_important = if is_id_important(config) { 247 | format!("{}) ", color_id(id)) 248 | } else { 249 | String::new() 250 | }; 251 | 252 | let mut message = match reason_kind { 253 | ReasonKind::Code => format!( 254 | "{}{}: code {} -> {}", 255 | &id_if_important, 256 | ¶meter, 257 | initial_response.code(), 258 | self.code(), 259 | ), 260 | ReasonKind::Text => format!( 261 | "{}{}: page {} -> {} ({})", 262 | &id_if_important, 263 | ¶meter, 264 | initial_response.text.len(), 265 | self.text.len().to_string().bright_yellow(), 266 | diff.unwrap() 267 | ), 268 | ReasonKind::Reflected => format!( 269 | "{}{}: {}", 270 | &id_if_important, 271 | "reflects".bright_blue(), 272 | parameter 273 | ), 274 | ReasonKind::NotReflected => format!( 275 | "{}{}: {}", 276 | &id_if_important, 277 | "changes reflections".bright_cyan(), 278 | parameter 279 | ), 280 | }; 281 | 282 | if config.verbose > 0 { 283 | if !config.save_responses.is_empty() { 284 | message += &format!(" [saved to {}]", save_request(config, self, parameter)?); 285 | } 286 | 287 | if config.disable_progress_bar { 288 | writeln!(io::stdout(), "{}", message).ok(); 289 | } else { 290 | progress_bar.println(message); 291 | } 292 | } else if !config.save_responses.is_empty() { 293 | save_request(config, self, parameter)?; 294 | } 295 | 296 | Ok(()) 297 | } 298 | 299 | fn kind(&self) -> Status { 300 | if self.code <= 199 { 301 | Status::Other 302 | } else if self.code <= 299 { 303 | Status::Ok 304 | } else if self.code <= 399 { 305 | Status::Redirect 306 | } else if self.code <= 499 { 307 | Status::UserFault 308 | } else if self.code <= 599 { 309 | Status::ServerFault 310 | } else { 311 | Status::Other 312 | } 313 | } 314 | 315 | /// returns self.code but with colors 316 | pub fn code(&self) -> String { 317 | match self.kind() { 318 | Status::Ok => self.code.to_string().bright_green().to_string(), 319 | Status::Redirect => self.code.to_string().bright_blue().to_string(), 320 | Status::UserFault => self.code.to_string().bright_yellow().to_string(), 321 | Status::ServerFault => self.code.to_string().bright_red().to_string(), 322 | Status::Other => self.code.to_string().magenta().to_string(), 323 | } 324 | } 325 | 326 | /// get possible parameters from the page itself 327 | pub fn get_possible_parameters(&self) -> Vec { 328 | let mut found: Vec = Vec::new(); 329 | let body = &self.text; 330 | 331 | let re_special_chars = Regex::new(r#"[\W]"#).unwrap(); 332 | 333 | let re_name = Regex::new(r#"(?i)name=("|')?"#).unwrap(); 334 | let re_inputs = Regex::new(r#"(?i)name=("|')?[\w-]+"#).unwrap(); 335 | for cap in re_inputs.captures_iter(body) { 336 | found.push(re_name.replace_all(&cap[0], "").to_string()); 337 | } 338 | 339 | let re_var = Regex::new(r#"(?i)(var|let|const)\s+?"#).unwrap(); 340 | let re_full_vars = Regex::new(r#"(?i)(var|let|const)\s+?[\w-]+"#).unwrap(); 341 | for cap in re_full_vars.captures_iter(body) { 342 | found.push(re_var.replace_all(&cap[0], "").to_string()); 343 | } 344 | 345 | let re_words_in_quotes = Regex::new(r#"("|')[a-zA-Z0-9]{3,20}('|")"#).unwrap(); 346 | for cap in re_words_in_quotes.captures_iter(body) { 347 | found.push(re_special_chars.replace_all(&cap[0], "").to_string()); 348 | } 349 | 350 | let re_words_within_objects = Regex::new(r#"[\{,]\s*[[:alpha:]]\w{2,25}:"#).unwrap(); 351 | for cap in re_words_within_objects.captures_iter(body) { 352 | found.push(re_special_chars.replace_all(&cap[0], "").to_string()); 353 | } 354 | 355 | found.sort(); 356 | found.dedup(); 357 | found 358 | } 359 | 360 | /// print the whole response 361 | pub fn print(&self) -> String { 362 | let http_version = match self.http_version { 363 | Some(val) => match val { 364 | http::Version::HTTP_09 => "HTTP/0.9", 365 | http::Version::HTTP_10 => "HTTP/1.0", 366 | http::Version::HTTP_11 => "HTTP/1.1", 367 | http::Version::HTTP_2 => "HTTP/2", 368 | http::Version::HTTP_3 => "HTTP/3", 369 | _ => "HTTP/x", 370 | }, 371 | None => "HTTP/x", 372 | }; 373 | 374 | format!("{} {} \n{}", http_version, self.code, self.text) 375 | } 376 | 377 | /// print the request and response 378 | pub fn print_all(&self) -> String { 379 | self.request.as_ref().unwrap().print_sent() + "\n\n" + &self.print() 380 | } 381 | } 382 | -------------------------------------------------------------------------------- /src/network/tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use tokio::time::Duration; 4 | 5 | use crate::network::{ 6 | request::{Request, RequestDefaults}, 7 | utils::{Headers, InjectionPlace, is_binary_content}, 8 | }; 9 | 10 | #[test] 11 | fn check_is_binary_content(){ 12 | assert!(is_binary_content(Some("application/pdf".to_string()))); 13 | assert!(is_binary_content(Some("something/zip".to_string()))); 14 | assert!(is_binary_content(Some("image/png".to_string()))); 15 | assert!(is_binary_content(Some("image/something".to_string()))); 16 | 17 | assert!(!is_binary_content(Some("application/json".to_string()))); 18 | assert!(!is_binary_content(Some("application/html".to_string()))); 19 | } 20 | 21 | #[test] 22 | fn query_creation() { 23 | let mut l = RequestDefaults::default(); 24 | l.template = "%k=payload".to_string(); 25 | l.joiner = "&".to_string(); 26 | let parameters = vec!["test1".to_string()]; 27 | let mut request = Request::new(&l, parameters); 28 | request.prepare(); 29 | 30 | assert_eq!(request.make_query(), "test1=payload"); 31 | } 32 | 33 | #[test] 34 | fn request_defaults_generation() { 35 | let defaults = RequestDefaults::new::( 36 | "GET", 37 | "https://example.com:8443/path", 38 | Vec::from([("X-Header".to_string(), "Value".to_string())]), 39 | Duration::from_millis(0), 40 | Default::default(), 41 | None, 42 | None, 43 | false, 44 | None, 45 | false, 46 | false, 47 | "", 48 | false, 49 | false, 50 | ) 51 | .unwrap(); 52 | 53 | assert_eq!(defaults.scheme, "https"); 54 | assert_eq!(defaults.host, "example.com"); 55 | assert_eq!(defaults.port, 8443); 56 | assert_eq!(defaults.path, "/path?%s"); 57 | assert_eq!( 58 | defaults.custom_headers.get_value("X-Header").unwrap(), 59 | "Value" 60 | ); 61 | assert_eq!(defaults.template, "%k=%v"); 62 | assert_eq!(defaults.joiner, "&"); 63 | assert_eq!(defaults.injection_place, InjectionPlace::Path); 64 | } 65 | 66 | #[test] 67 | fn json_request_body_generation() { 68 | let defaults = RequestDefaults::new::( 69 | "POST", 70 | "https://example.com:8443/path", 71 | Vec::from([("X-Header".to_string(), "Value".to_string())]), 72 | Duration::from_millis(0), 73 | Default::default(), 74 | None, 75 | None, 76 | false, 77 | None, 78 | false, 79 | false, 80 | "{\"something\":1}", 81 | false, 82 | false, 83 | ) 84 | .unwrap(); 85 | 86 | assert!(defaults.is_json); 87 | assert_eq!(defaults.body, "{\"something\":1, %s}"); 88 | assert_eq!(defaults.template, "\"%k\": %v"); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/network/utils.rs: -------------------------------------------------------------------------------- 1 | use std::{time::Duration, error::Error}; 2 | 3 | use lazy_static::lazy_static; 4 | use percent_encoding::{AsciiSet, CONTROLS}; 5 | use regex::Regex; 6 | use reqwest::Client; 7 | use serde::Serialize; 8 | 9 | use crate::{config::structs::Config, utils::random_line}; 10 | 11 | use super::response::Response; 12 | 13 | lazy_static! { 14 | /// characters to encode in case --encode option provided 15 | pub static ref FRAGMENT: AsciiSet = CONTROLS 16 | .add(b' ') 17 | .add(b'"') 18 | .add(b'<') 19 | .add(b'>') 20 | .add(b'`') 21 | .add(b'&') 22 | .add(b'#') 23 | .add(b';') 24 | .add(b'/') 25 | .add(b'=') 26 | .add(b'%'); 27 | } 28 | 29 | /// enum mainly created for the correct json parsing 30 | #[derive(Debug, Clone, PartialEq, Eq)] 31 | pub enum DataType { 32 | /// we need a different data type for json because some json values can be used without quotes (numbers, booleans, ..) 33 | /// and therefore this type should be treated differently 34 | Json, 35 | 36 | /// that's from parsed request's content-type header 37 | /// needs to be ignored in case the injection points not within the body 38 | /// to exclude false positive /?{"ZXxZPLN":"ons9XDZ", ..} or Cookie: {"ZXxZPLN":"ons9XDZ", ..} queries 39 | // it still can be bypassed with the correct --data-type argument 40 | ProbablyJson, 41 | 42 | Urlencoded, 43 | Headers 44 | } 45 | 46 | /// where to insert parameters 47 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Copy)] 48 | pub enum InjectionPlace { 49 | Path, 50 | Body, 51 | Headers, 52 | HeaderValue, 53 | } 54 | 55 | impl Default for InjectionPlace { 56 | fn default() -> Self { InjectionPlace::Path } 57 | } 58 | 59 | pub trait Headers { 60 | fn contains_key(&self, key: &str) -> bool; 61 | fn get_index_case_insensitive(&self, key: &str) -> Option; 62 | fn get_value(&self, key: &str) -> Option; 63 | fn get_value_case_insensitive(&self, key: &str) -> Option; 64 | } 65 | 66 | impl Headers for Vec<(String, String)> { 67 | fn contains_key(&self, key: &str) -> bool { 68 | for (k, _) in self.iter() { 69 | if k == key { 70 | return true; 71 | } 72 | } 73 | false 74 | } 75 | 76 | fn get_index_case_insensitive(&self, key: &str) -> Option { 77 | self.iter().position(|r| r.0.to_lowercase() == key.to_ascii_lowercase()) 78 | } 79 | 80 | fn get_value(&self, key: &str) -> Option { 81 | for (k, v) in self.iter() { 82 | if k == key { 83 | return Some(v.to_owned()); 84 | } 85 | } 86 | None 87 | } 88 | 89 | fn get_value_case_insensitive(&self, key: &str) -> Option { 90 | let key = key.to_lowercase(); 91 | for (k, v) in self.iter() { 92 | if k.to_lowercase() == key { 93 | return Some(v.to_owned()); 94 | } 95 | } 96 | None 97 | } 98 | } 99 | 100 | /// writes request and response to a file 101 | /// return file location 102 | pub(super) fn save_request( 103 | config: &Config, 104 | response: &Response, 105 | param_key: &str, 106 | ) -> Result> { 107 | let output = response.print_all(); 108 | 109 | let filename = format!( 110 | "{}/{}-{}-{}-{}", 111 | &config.save_responses, 112 | &response.request.as_ref().unwrap().defaults.host, 113 | response 114 | .request 115 | .as_ref() 116 | .unwrap() 117 | .defaults 118 | .method 119 | .to_lowercase(), 120 | param_key, 121 | random_line(3) //nonce to prevent overwrites 122 | ); 123 | 124 | std::fs::write(&filename, output)?; 125 | 126 | Ok(filename) 127 | } 128 | 129 | pub fn create_client(config: &Config, replay: bool) -> Result> { 130 | let mut client = Client::builder() 131 | .danger_accept_invalid_certs(true) 132 | .timeout(Duration::from_secs(config.timeout as u64)) 133 | .http1_title_case_headers() 134 | .cookie_store(true) 135 | .http09_responses() 136 | .use_rustls_tls(); 137 | 138 | if config.disable_trustdns { 139 | client = client.no_trust_dns(); 140 | } 141 | 142 | if replay { 143 | client = client.proxy(match reqwest::Proxy::all(&config.replay_proxy) { 144 | Ok(val) => val, 145 | Err(err) => { 146 | Err(format!("Unable to parse replay_proxy: {}", err))? 147 | } 148 | }); 149 | } else { 150 | if !config.proxy.is_empty() { 151 | client = client.proxy(reqwest::Proxy::all(&config.proxy)?); 152 | } 153 | } 154 | 155 | if !config.follow_redirects { 156 | client = client.redirect(reqwest::redirect::Policy::none()); 157 | } 158 | 159 | if config.http_version.is_some() { 160 | match config.http_version { 161 | Some(http::Version::HTTP_11) => client = client.http1_only(), 162 | Some(http::Version::HTTP_2) => client = client.http2_prior_knowledge(), 163 | _ => unreachable!() 164 | } 165 | } 166 | 167 | Ok(client.build()?) 168 | } 169 | 170 | /// check whether the content is binary 171 | /// so we can ignore the body in comparing 172 | /// a few reasons for it: 173 | /// 1. the comparing of binary content takes a lot of time 174 | /// 2. page diff anyway will be checked by the content-length header 175 | /// because the content-length header usually static for binary files 176 | pub fn is_binary_content(content_type: Option) -> bool { 177 | lazy_static!{ 178 | static ref RE_BINARY_MIME: Regex = Regex::new( 179 | "((video|audio|font|image)/\ 180 | |\ 181 | /(zip|octet-stream|x-tar|vnd\\.rar|pdf|gzip|epub-zip|x-bzip|x-bzip2|x-freearc|x-7z-compressed))" 182 | ).unwrap(); 183 | } 184 | 185 | content_type.is_some() && RE_BINARY_MIME.is_match(&content_type.unwrap()) 186 | } -------------------------------------------------------------------------------- /src/runner/logic.rs: -------------------------------------------------------------------------------- 1 | use std::{cmp, collections::HashMap, error::Error, sync::Arc}; 2 | 3 | use async_recursion::async_recursion; 4 | use futures::stream::StreamExt; 5 | use parking_lot::Mutex; 6 | 7 | use crate::{ 8 | network::request::Request, 9 | runner::utils::{FoundParameter, ReasonKind}, utils::progress_style_check_requests, 10 | }; 11 | 12 | use super::runner::Runner; 13 | 14 | /// impl logic for checking parameters 15 | impl<'a> Runner<'a> { 16 | /// just splits params into two parts and runs check_parameters_recursion for every part 17 | async fn repeat( 18 | &self, 19 | shared_diffs: Arc>>, 20 | shared_green_lines: Arc>>, 21 | shared_found_params: Arc>>, 22 | mut params: Vec, 23 | ) -> Result<(), Box> { 24 | let second_params_part = params.split_off(params.len() / 2); 25 | 26 | self.check_parameters_recursion( 27 | Arc::clone(&shared_diffs), 28 | Arc::clone(&shared_green_lines), 29 | Arc::clone(&shared_found_params), 30 | params, 31 | ) 32 | .await?; 33 | self.check_parameters_recursion( 34 | shared_diffs, 35 | shared_green_lines, 36 | shared_found_params, 37 | second_params_part, 38 | ) 39 | .await 40 | } 41 | 42 | #[async_recursion(?Send)] 43 | async fn check_parameters_recursion( 44 | &self, 45 | shared_diffs: Arc>>, 46 | shared_green_lines: Arc>>, 47 | shared_found_params: Arc>>, 48 | mut params: Vec, 49 | ) -> Result<(), Box> { 50 | let request = Request::new(&self.request_defaults, params.clone()); 51 | let mut response = match request.clone().wrapped_send().await { 52 | Ok(val) => val, 53 | Err(_) => match Request::new_random(&self.request_defaults, params.len()) 54 | .send() 55 | .await 56 | { 57 | //we don't return the actual response because it was a random request without original parameters 58 | //instead we return an empty response from the original request 59 | Ok(_) => request.empty_response(), 60 | //looks like either server or network is down 61 | Err(err) => Err(format!("Unable to reach server ({})", err))?, 62 | }, 63 | }; 64 | 65 | if self.stable.reflections { 66 | response.fill_reflected_parameters(&self.initial_response); 67 | 68 | let (reflected_parameter, repeat) = response.proceed_reflected_parameters(); 69 | 70 | if let Some(reflected_parameter) = reflected_parameter { 71 | 72 | let mut found_params = shared_found_params.lock(); 73 | if !found_params.iter().any(|x| x.name == reflected_parameter) { 74 | let mut kind = ReasonKind::Reflected; 75 | // explained in response.proceed_reflected_parameters() method 76 | // chunk.len() == 1 and not 2 because the random parameter appends later 77 | if params.len() == 1 { 78 | kind = ReasonKind::NotReflected; 79 | } 80 | 81 | found_params.push(FoundParameter::new( 82 | reflected_parameter, 83 | &vec![], 84 | response.code, 85 | response.text.len(), 86 | kind.clone(), 87 | )); 88 | drop(found_params); 89 | 90 | // remove found parameter from the list 91 | params.remove( 92 | params 93 | .iter() 94 | .position(|x| *x == reflected_parameter) 95 | .unwrap(), 96 | ); 97 | 98 | response.write_and_save( 99 | self.id, 100 | self.config, 101 | &self.initial_response, 102 | kind, 103 | reflected_parameter, 104 | None, 105 | self.progress_bar, 106 | )?; 107 | } 108 | } 109 | 110 | if repeat { 111 | return self 112 | .repeat( 113 | shared_diffs, 114 | shared_green_lines, 115 | shared_found_params, 116 | params.clone(), 117 | ) 118 | .await; 119 | } 120 | 121 | if self.config.reflected_only { 122 | return Ok(()); 123 | } 124 | } 125 | 126 | if self.initial_response.code != response.code { 127 | // increases the specific response code counter 128 | // helps to notice whether the page's completely changed 129 | // like, for example, when the IP got banned by the server 130 | { 131 | let mut green_lines = shared_green_lines.lock(); 132 | match green_lines.get(&response.code.to_string()) { 133 | Some(val) => { 134 | let n_val = *val; 135 | green_lines.insert(response.code.to_string(), n_val + 1); 136 | if n_val > 50 { 137 | drop(green_lines); 138 | 139 | let check_response = 140 | Request::new_random(&self.request_defaults, params.len()) 141 | .wrapped_send() 142 | .await 143 | .unwrap_or_default(); 144 | 145 | if check_response.code != self.initial_response.code { 146 | return Err(format!( 147 | "{} The page became unstable (code)", 148 | self.request_defaults.url() 149 | ))?; 150 | } else { 151 | let mut green_lines = shared_green_lines.lock(); 152 | green_lines.insert(response.code.to_string(), 0); 153 | } 154 | } 155 | } 156 | _ => { 157 | green_lines.insert(response.code.to_string(), 0); 158 | } 159 | } 160 | } 161 | 162 | // there's only 1 parameter left that's changing the page's code 163 | if params.len() == 1 { 164 | response.write_and_save( 165 | self.id, 166 | self.config, 167 | &self.initial_response, 168 | ReasonKind::Code, 169 | ¶ms[0], 170 | None, 171 | self.progress_bar, 172 | )?; 173 | 174 | let mut found_params = shared_found_params.lock(); 175 | found_params.push(FoundParameter::new( 176 | ¶ms[0], 177 | &vec![format!( 178 | "{} -> {}", 179 | &self.initial_response.code, response.code 180 | )], 181 | response.code, 182 | response.text.len(), 183 | ReasonKind::Code, 184 | )); 185 | // there's more than 1 parameter left - split the list and repeat 186 | } else { 187 | return self 188 | .repeat( 189 | shared_diffs, 190 | shared_green_lines, 191 | shared_found_params, 192 | params.clone(), 193 | ) 194 | .await; 195 | } 196 | } else if self.stable.body { 197 | // check whether the new_diff has at least 1 unique diff compared to stored diffs 198 | let (_, new_diffs) = { 199 | let diffs = shared_diffs.lock(); 200 | response.compare(&self.initial_response, &diffs)? 201 | }; 202 | 203 | // and then make a new request to check whether it's a permament diff or not 204 | if !new_diffs.is_empty() { 205 | if self.config.strict { 206 | let found_params = shared_found_params.lock(); 207 | if found_params.iter().any(|x| x.diffs == new_diffs.join("|")) { 208 | return Ok(()); 209 | } 210 | } 211 | 212 | // just request the page with random parameters and store it's diffs 213 | // maybe I am overcheking this, but still to be sure.. 214 | let tmp_resp = Request::new_random(&self.request_defaults, params.len()) 215 | .send() 216 | .await?; 217 | 218 | let (_, tmp_diffs) = { 219 | let diffs = shared_diffs.lock(); 220 | tmp_resp.compare(&self.initial_response, &diffs)? 221 | }; 222 | 223 | let mut diffs = shared_diffs.lock(); 224 | for diff in tmp_diffs { 225 | diffs.push(diff); 226 | } 227 | } 228 | 229 | let diffs = shared_diffs.lock(); 230 | 231 | // check whether the page still(after making a random request and storing it's diffs) has an unique diffs 232 | for diff in new_diffs.iter() { 233 | if !diffs.contains(diff) { 234 | let mut found_params = shared_found_params.lock(); 235 | 236 | // there's only one parameter left that changing the page 237 | if params.len() == 1 && !found_params.iter().any(|x| x.name == params[0]) { 238 | // repeating --strict checks. We need to do it twice because we're usually running in parallel 239 | // and some parameters may be found after the first check 240 | if self.config.strict && found_params.iter().any(|x| x.diffs == new_diffs.join("|")) { 241 | return Ok(()); 242 | } 243 | 244 | response.write_and_save( 245 | self.id, 246 | self.config, 247 | &self.initial_response, 248 | ReasonKind::Text, 249 | ¶ms[0], 250 | Some(diff), 251 | self.progress_bar, 252 | )?; 253 | 254 | found_params.push(FoundParameter::new( 255 | ¶ms[0], 256 | &new_diffs, 257 | response.code, 258 | response.text.len(), 259 | ReasonKind::Text, 260 | )); 261 | break; 262 | // we don't know what parameter caused the difference in response yet 263 | // so we are repeating 264 | } else { 265 | drop(diffs); 266 | drop(found_params); 267 | return self 268 | .repeat( 269 | shared_diffs, 270 | shared_green_lines, 271 | shared_found_params, 272 | params.clone(), 273 | ) 274 | .await; 275 | } 276 | } 277 | } 278 | } 279 | 280 | Ok(()) 281 | } 282 | 283 | /// check parameters in a loop chunk by chunk 284 | pub async fn check_parameters( 285 | &self, 286 | params: &Vec, 287 | ) -> Result<(Vec, Vec), Box> { 288 | let max = cmp::min(self.max, params.len()); 289 | 290 | // the amount of requests needed for process all the parameters 291 | let all = params.len() / max; 292 | 293 | // change and reset the progress bar 294 | self.prepare_progress_bar(progress_style_check_requests(self.config), all + 1); 295 | 296 | // wrap the variables to share them between futures 297 | let mut diffs = self.diffs.clone(); 298 | let mut green_lines = HashMap::new(); 299 | let mut found_params = Vec::new(); 300 | 301 | let shared_diffs = Arc::new(Mutex::new(&mut diffs)); 302 | let shared_green_lines = Arc::new(Mutex::new(&mut green_lines)); 303 | let shared_found_params = Arc::new(Mutex::new(&mut found_params)); 304 | 305 | let _futures_data = futures::stream::iter(params.chunks(max).map(|chunk| { 306 | let shared_diffs = Arc::clone(&shared_diffs); 307 | let shared_green_lines = Arc::clone(&shared_green_lines); 308 | let shared_found_params = Arc::clone(&shared_found_params); 309 | 310 | async move { 311 | self.progress_bar.inc(1); 312 | 313 | self.check_parameters_recursion( 314 | shared_diffs, 315 | shared_green_lines, 316 | shared_found_params, 317 | chunk.to_vec(), 318 | ) 319 | .await 320 | } 321 | })) 322 | .buffer_unordered(self.config.concurrency) 323 | .collect::>>>() 324 | .await; 325 | 326 | Ok((diffs, found_params)) 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /src/runner/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod logic; 2 | pub mod output; 3 | pub mod runner; 4 | pub mod utils; 5 | -------------------------------------------------------------------------------- /src/runner/output.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | use colored::Colorize; 3 | 4 | use crate::{ 5 | config::structs::Config, 6 | network::{ 7 | request::{Request, RequestDefaults}, 8 | response::Response, 9 | utils::InjectionPlace, 10 | }, 11 | }; 12 | 13 | use super::utils::FoundParameter; 14 | 15 | #[derive(Debug, Serialize)] 16 | pub struct RunnerOutput { 17 | /// request's method 18 | pub method: String, 19 | 20 | /// request url without injection point 21 | pub url: String, 22 | 23 | /// initial response code 24 | pub status: u16, 25 | 26 | /// initial response size (body + headers) 27 | pub size: usize, 28 | 29 | pub found_params: Vec, 30 | 31 | pub injection_place: InjectionPlace, 32 | 33 | 34 | /// prepared query with found parameters 35 | #[serde(skip_serializing)] 36 | pub query: String, 37 | 38 | /// prepared request with found parameters 39 | #[serde(skip_serializing)] 40 | pub request: String, 41 | } 42 | 43 | pub trait ParseOutputs { 44 | fn parse_output(&self, config: &Config) -> String; 45 | } 46 | 47 | impl RunnerOutput { 48 | pub fn new( 49 | request_defaults: &RequestDefaults, 50 | initial_response: &Response, 51 | found_params: Vec, 52 | ) -> Self { 53 | Self { 54 | method: request_defaults.method.clone(), 55 | //remove injection point in case the injection point within url 56 | url: if request_defaults.injection_place == InjectionPlace::Path { 57 | request_defaults.url_without_default_port().replace("?%s", "").replace("&%s", "") 58 | } else { 59 | request_defaults.url_without_default_port() 60 | }, 61 | status: initial_response.code, 62 | size: initial_response.text.len(), 63 | found_params, 64 | injection_place: request_defaults.injection_place, 65 | query: String::new(), 66 | request: String::new(), 67 | } 68 | } 69 | 70 | /// fills self.request and self.query if they're needed for output 71 | pub fn prepare(&mut self, config: &Config, request_defaults: &RequestDefaults) { 72 | if config.output_format == "url" || config.output_format == "request" { 73 | let mut request = Request::new( 74 | request_defaults, 75 | self.found_params 76 | .iter() 77 | .map( 78 | //in case a parameter has a non standart value (like 'true') 79 | //it should be treated differently (=true) should be added 80 | //otherwise that parameter will have random value 81 | |x| { 82 | if x.value.is_none() { 83 | x.name.to_owned() 84 | } else { 85 | format!("{}={}", x.name, x.value.as_ref().unwrap()) 86 | } 87 | }, 88 | ) 89 | .collect(), 90 | ); 91 | 92 | request.prepare(); 93 | 94 | if config.output_format == "url" { 95 | self.query = request.make_query(); 96 | } else { 97 | self.request = request.print(); 98 | } 99 | } 100 | } 101 | 102 | /// parses the runner output struct to one specified in config format 103 | pub fn parse(&self, config: &Config) -> String { 104 | match config.output_format.as_str() { 105 | "url" => { 106 | //make line an url with injection point 107 | let line = if !self.found_params.is_empty() 108 | && self.injection_place == InjectionPlace::Path 109 | { 110 | if !self.url.contains('?') { 111 | self.url.clone() + "?%s" 112 | } else { 113 | self.url.clone() + "&%s" 114 | } 115 | } else { 116 | self.url.clone() 117 | }; 118 | 119 | (line).replace("%s", &self.query) 120 | } 121 | 122 | "request" => self.request.clone(), 123 | 124 | _ => { 125 | format!( 126 | "{} {} % {}", 127 | &self.method.blue(), 128 | &self.url, 129 | self.found_params 130 | .iter() 131 | .map(|x| x.get_colored()) 132 | .collect::>() 133 | .join(", ") 134 | ) 135 | } 136 | } 137 | } 138 | } 139 | 140 | impl ParseOutputs for Vec { 141 | fn parse_output(&self, config: &Config) -> String { 142 | // print an array of json objects instead of just new line separeted new objects 143 | if config.output_format.as_str() == "json" { 144 | serde_json::to_string(&self).unwrap() 145 | // otherwise calls .parse on every RunnerOutput 146 | } else { 147 | self.iter() 148 | .map(|x| x.parse(config)) 149 | .collect::>() 150 | .join("") 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/runner/runner.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, io::{self, Write}}; 2 | 3 | use colored::Colorize; 4 | use indicatif::{ProgressBar, ProgressStyle}; 5 | 6 | use crate::{ 7 | config::structs::Config, 8 | network::{ 9 | request::{Request, RequestDefaults}, 10 | response::Response, 11 | utils::{create_client, InjectionPlace}, 12 | }, 13 | utils::{self, color_id, random_line, progress_style_learn_requests, is_id_important}, 14 | DEFAULT_PROGRESS_URL_MAX_LEN, MAX_PAGE_SIZE, 15 | }; 16 | 17 | use super::{ 18 | output::RunnerOutput, 19 | utils::{fold_url, replay, verify, FoundParameter, Parameters, Stable}, 20 | }; 21 | 22 | pub struct Runner<'a> { 23 | /// unique id of the runner to distinguish output between different urls 24 | pub id: usize, 25 | 26 | pub config: &'a Config, 27 | 28 | /// request data to create the request object 29 | pub request_defaults: RequestDefaults, 30 | 31 | /// parameters found by scraping words from the page 32 | pub possible_params: Vec, 33 | 34 | /// the max amount of parameters to send per request 35 | pub max: usize, 36 | 37 | /// whether body or/and reflections are stable 38 | pub stable: Stable, 39 | 40 | /// initial response to compare with 41 | pub initial_response: Response<'a>, 42 | 43 | /// page's diffs for the current url|method pair 44 | pub diffs: Vec, 45 | 46 | /// progress bar object to print progress bar & found parameters 47 | pub progress_bar: &'a ProgressBar, 48 | } 49 | 50 | impl<'a> Runner<'a> { 51 | /// creates a runner, makes an initial response 52 | pub async fn new( 53 | config: &'a Config, 54 | request_defaults: &'a mut RequestDefaults, 55 | progress_bar: &'a ProgressBar, 56 | id: usize, 57 | ) -> Result, Box> { 58 | // make first request and collect some information like code, reflections, possible parameters 59 | // we are making another request defaults because the original one will be changed right after 60 | let mut temp_request_defaults = request_defaults.clone(); 61 | 62 | // we need a random_parameter with a long value in order to increase accuracy while determining the default amount of reflections 63 | let mut random_parameter = vec![(random_line(10), random_line(10))]; 64 | 65 | temp_request_defaults 66 | .parameters 67 | .append(&mut random_parameter); 68 | 69 | let initial_response = Request::new(&temp_request_defaults, vec![]).send().await?; 70 | 71 | // add possible parameters to the list of parameters in case the injection place is not headers 72 | let possible_params = if request_defaults.injection_place != InjectionPlace::Headers { 73 | initial_response.get_possible_parameters() 74 | } else { 75 | Vec::new() 76 | }; 77 | 78 | // find how many times was the random parameter reflected 79 | request_defaults.amount_of_reflections = 80 | initial_response.count(&temp_request_defaults.parameters.first().unwrap().1); 81 | 82 | // some "magic" to be able to return initial_response 83 | // otherwise throws lifetime errors 84 | // turns out you can't simple do 'initial_response.request = None'. 85 | let initial_response = Response { 86 | time: initial_response.time, 87 | code: initial_response.code, 88 | headers: initial_response.headers, 89 | text: initial_response.text, 90 | reflected_parameters: initial_response.reflected_parameters, 91 | request: None, 92 | http_version: initial_response.http_version, 93 | }; 94 | 95 | Ok(Runner { 96 | config, 97 | request_defaults: request_defaults.clone(), 98 | possible_params, 99 | max: 0, //to be filled later, in stability-checker() 100 | stable: Default::default(), 101 | initial_response, 102 | diffs: Vec::new(), 103 | progress_bar, 104 | id, 105 | }) 106 | } 107 | 108 | /// actually runs the runner 109 | pub async fn run(mut self, params: &mut Vec) -> Result> { 110 | self.write_banner_url(); 111 | 112 | // makes a few request to check page's behavior 113 | self.stability_checker().await?; 114 | 115 | if self.config.max.is_none() { 116 | utils::info( 117 | self.config, 118 | self.id, 119 | self.progress_bar, 120 | "info", 121 | format!("Amount of parameters per request - {}", self.max), 122 | ); 123 | } 124 | 125 | // add only unique possible params to the vec of all params (the tool works properly only with unique parameters) 126 | // less efficient than making it within the sorted vec but I want to preserve the order 127 | for param in self.possible_params.iter() { 128 | if !params.contains(param) { 129 | params.push(param.to_owned()); 130 | } 131 | } 132 | 133 | // try to find existing parameters from the list 134 | let (diffs, mut found_params) = if !params.is_empty() { 135 | self.check_parameters(params).await? 136 | } else { 137 | utils::info( 138 | self.config, 139 | self.id, 140 | self.progress_bar, 141 | "info", 142 | "No parameters were provided", 143 | ); 144 | (Vec::new(), Vec::new()) 145 | }; 146 | 147 | self.check_non_random_parameters(&mut found_params).await?; 148 | 149 | // remove duplicates 150 | let mut found_params = found_params.process(self.request_defaults.injection_place); 151 | 152 | // verify found parameters 153 | if self.config.verify { 154 | found_params = if let Ok(filtered_params) = verify( 155 | &self.initial_response, 156 | &self.request_defaults, 157 | &found_params, 158 | &diffs, 159 | &self.stable, 160 | ) 161 | .await 162 | { 163 | filtered_params 164 | } else { 165 | utils::info( 166 | self.config, 167 | self.id, 168 | self.progress_bar, 169 | "~", 170 | "was unable to verify found parameters", 171 | ); 172 | found_params 173 | }; 174 | } 175 | 176 | // replay request with found parameters via another proxy 177 | if !self.config.replay_proxy.is_empty() { 178 | 179 | let client = match create_client(self.config, true) { 180 | Ok(val) => Some(val), 181 | Err(err) => { 182 | utils::info( 183 | self.config, 184 | self.id, 185 | self.progress_bar, 186 | "~", 187 | err, 188 | ); 189 | 190 | None 191 | } 192 | }; 193 | 194 | if client.is_some() { 195 | if replay( 196 | self.config, 197 | &self.request_defaults, 198 | &client.unwrap(), 199 | &found_params, 200 | ).await 201 | .is_err() { 202 | utils::info( 203 | self.config, 204 | self.id, 205 | self.progress_bar, 206 | "~", 207 | "was unable to resend found parameters via another proxy", 208 | ); 209 | } 210 | } 211 | } 212 | 213 | Ok(RunnerOutput::new( 214 | &self.request_defaults, 215 | &self.initial_response, 216 | found_params, 217 | )) 218 | } 219 | 220 | /// check parameters with non random values 221 | async fn check_non_random_parameters( 222 | &self, 223 | found_params: &mut Vec, 224 | ) -> Result<(), Box> { 225 | if !self.request_defaults.disable_custom_parameters { 226 | let mut custom_parameters = self.config.custom_parameters.clone(); 227 | let mut params = Vec::new(); 228 | 229 | // in a loop check common parameters like debug, admin, .. with common values true, 1, false.. 230 | // until there's no values left 231 | loop { 232 | for (k, v) in custom_parameters.iter_mut() { 233 | //do not request parameters that already have been found 234 | if found_params 235 | .iter() 236 | .map(|x| x.name.split('=').next().unwrap()) 237 | .any(|x| x == k) 238 | { 239 | continue; 240 | } 241 | 242 | if !v.is_empty() { 243 | params.push([k.as_str(), "=", v.pop().unwrap().as_str()].concat()); 244 | } 245 | } 246 | 247 | if params.is_empty() { 248 | break; 249 | } 250 | 251 | found_params.append(&mut self.check_parameters(¶ms).await?.1); 252 | params.clear(); 253 | } 254 | } 255 | 256 | Ok(()) 257 | } 258 | 259 | /// makes several requests in order to learn how the page behaves 260 | /// tries to increase the max amount of parameters per request in case the default value not changed 261 | async fn stability_checker(&mut self) -> Result<(), Box> { 262 | // guess or get from the user the amount of parameters to send per request 263 | let default_max = match self.config.max { 264 | Some(var) => var as isize, 265 | None => match self.request_defaults.injection_place { 266 | InjectionPlace::Body => -512, 267 | InjectionPlace::Path => self.try_to_guess_the_right_max_for_query().await?, 268 | InjectionPlace::Headers => -64, 269 | InjectionPlace::HeaderValue => -64, 270 | }, 271 | }; 272 | 273 | self.max = default_max.unsigned_abs(); 274 | 275 | // make a few requests and collect all persistent diffs, check for stability 276 | self.empty_reqs().await?; 277 | 278 | if self.config.reflected_only && !self.stable.reflections { 279 | Err("Reflections are not stable")?; 280 | } 281 | 282 | // check whether it is possible to use 192 or 256 params in a single request instead of 128 default 283 | if default_max == -128 { 284 | self.try_to_increase_max().await?; 285 | } 286 | 287 | Ok(()) 288 | } 289 | 290 | /// makes first requests and checks page behavior 291 | /// fills self.diffs and self.stable 292 | pub async fn empty_reqs(&mut self) -> Result<(), Box> { 293 | let mut stable = Stable { 294 | body: true, 295 | reflections: true, 296 | }; 297 | let mut diffs: Vec = Vec::new(); 298 | 299 | // set up progress bar 300 | self.prepare_progress_bar(progress_style_learn_requests(self.config), self.config.learn_requests_count); 301 | 302 | for _ in 0..self.config.learn_requests_count { 303 | // to increase stability 304 | tokio::time::sleep(tokio::time::Duration::from_millis(150)).await; 305 | 306 | let response = Request::new_random(&self.request_defaults, self.max) 307 | .send() 308 | .await?; 309 | 310 | self.progress_bar.inc(1); 311 | 312 | // do not check pages >25MB because usually its just a binary file or sth 313 | if response.text.len() > MAX_PAGE_SIZE && !self.config.force { 314 | Err("The page's size > 25MB. Use --force flag to disable this error")?; 315 | } 316 | 317 | if !response.reflected_parameters.is_empty() { 318 | stable.reflections = false; 319 | } 320 | 321 | let (is_code_diff, mut new_diffs) = response.compare(&self.initial_response, &diffs)?; 322 | 323 | if is_code_diff { 324 | Err("The page is not stable (code)")? 325 | } 326 | 327 | diffs.append(&mut new_diffs); 328 | } 329 | 330 | // check the last time 331 | let response = Request::new_random(&self.request_defaults, self.max) 332 | .send() 333 | .await?; 334 | 335 | // in case the page is still different from other random ones - the body isn't stable 336 | if !response 337 | .compare(&self.initial_response, &diffs)? 338 | .1 339 | .is_empty() 340 | { 341 | utils::info( 342 | self.config, 343 | self.id, 344 | self.progress_bar, 345 | "~", 346 | "The page is not stable (body)", 347 | ); 348 | stable.body = false; 349 | } 350 | 351 | (self.diffs, self.stable) = (diffs, stable); 352 | 353 | Ok(()) 354 | } 355 | 356 | /// checks whether the increasing of the amount of parameters changes the page 357 | /// changes self.max in case the page is stable with more parameters per request 358 | pub async fn try_to_increase_max(&mut self) -> Result<(), Box> { 359 | 360 | let delta = self.max / 2; 361 | 362 | let response = Request::new_random(&self.request_defaults, self.max + delta) 363 | .send() 364 | .await?; 365 | 366 | let (is_code_different, new_diffs) = 367 | response.compare(&self.initial_response, &self.diffs)?; 368 | let mut is_the_body_the_same = true; 369 | 370 | if !new_diffs.is_empty() { 371 | is_the_body_the_same = false; 372 | } 373 | 374 | // in case the page isn't different from previous one - try to increase max amount of parameters by 128 375 | if !is_code_different && (!self.stable.body || is_the_body_the_same) { 376 | let response = Request::new_random(&self.request_defaults, self.max + delta*2) 377 | .send() 378 | .await?; 379 | 380 | let (is_code_different, new_diffs) = 381 | response.compare(&self.initial_response, &self.diffs)?; 382 | 383 | if !new_diffs.is_empty() { 384 | is_the_body_the_same = false; 385 | } 386 | 387 | if !is_code_different && (!self.stable.body || is_the_body_the_same) { 388 | self.max += delta*2 389 | } else { 390 | self.max += delta 391 | } 392 | } 393 | 394 | Ok(()) 395 | } 396 | 397 | /// tries to detect the right amount of parameters that can be send per request in query 398 | /// TODO maybe detect based on reflection as well 399 | pub async fn try_to_guess_the_right_max_for_query(&mut self) -> Result> { 400 | 401 | let mut max = 128; 402 | 403 | let mut response = match Request::new_random(&self.request_defaults, max) 404 | .send() 405 | .await { 406 | Ok(val) => val, 407 | // some servers may cut connection in case url is too long 408 | // that's why we assume that this request returned response with status code = 0. 409 | Err(_) => { 410 | Request::empty_response(Request::new_random(&self.request_defaults, 0)) 411 | } 412 | }; 413 | 414 | loop { 415 | // the choosen max is okay 416 | if self.initial_response.code == response.code { 417 | break 418 | } 419 | 420 | if Request::new_random(&self.request_defaults, 0).send().await?.code != self.initial_response.code { 421 | Err("The page became unstable (code)")? 422 | }; 423 | 424 | max /= 2; 425 | 426 | if max < 4 { 427 | Err("Unable to guess the max amount of parameters per request. Try to use --max command line argument.")? 428 | } 429 | 430 | response = match Request::new_random(&self.request_defaults, max) 431 | .send() 432 | .await { 433 | Ok(val) => val, 434 | Err(_) => { 435 | Request::empty_response(Request::new_random(&self.request_defaults, 0)) 436 | } 437 | }; 438 | } 439 | 440 | Ok(max as isize *-1) 441 | } 442 | 443 | pub fn prepare_progress_bar(&self, sty: ProgressStyle, length: usize) { 444 | self.progress_bar.reset(); 445 | self.progress_bar.set_prefix(self.make_progress_prefix()); 446 | self.progress_bar.set_style(sty); 447 | self.progress_bar.set_length(length as u64); 448 | } 449 | 450 | fn make_progress_prefix(&self) -> String { 451 | // to align all the progress bars 452 | let id = if is_id_important(self.config) { 453 | let mut id = self.id.to_string() + ":"; 454 | id += &" ".repeat(1 + self.config.urls.len().to_string().len() - id.to_string().len()); 455 | format!("{} ", id.replace(&self.id.to_string(), &color_id(self.id))) 456 | } else { 457 | String::new() 458 | }; 459 | 460 | let mut method = self.request_defaults.method.clone(); 461 | method += 462 | &" ".repeat(self.config.methods.iter().map(|x| x.len()).max().unwrap() - method.len()); 463 | 464 | format!( 465 | "{}{} {}", 466 | id, 467 | method.blue(), 468 | fold_url( 469 | &self.request_defaults.url_without_default_port(), 470 | DEFAULT_PROGRESS_URL_MAX_LEN 471 | ) 472 | .green() 473 | ) 474 | } 475 | 476 | pub fn write_banner_url(&self) { 477 | 478 | let id = if is_id_important(self.config) { 479 | format!("[{}] ", color_id(self.id)) 480 | } else { 481 | String::new() 482 | }; 483 | 484 | let msg = format!( 485 | "{}{} {} ({}) [{}] {{{}}}", 486 | id, 487 | self.request_defaults.method.blue(), 488 | self.request_defaults.url_without_default_port().green(), 489 | self.initial_response.code(), 490 | self.initial_response.text.len().to_string().green(), 491 | self.request_defaults 492 | .amount_of_reflections 493 | .to_string() 494 | .magenta() 495 | ); 496 | 497 | if self.config.disable_progress_bar { 498 | writeln!(io::stdout(), "{}", msg).ok(); 499 | } else { 500 | self.progress_bar.println(msg); 501 | } 502 | } 503 | } 504 | -------------------------------------------------------------------------------- /src/runner/utils.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | error::Error, 3 | }; 4 | 5 | use lazy_static::lazy_static; 6 | use regex::Regex; 7 | use reqwest::Client; 8 | use serde::Serialize; 9 | use colored::Colorize; 10 | 11 | use crate::{ 12 | config::structs::Config, 13 | network::{ 14 | request::{Request, RequestDefaults}, 15 | response::Response, 16 | utils::InjectionPlace, 17 | }, 18 | utils::random_line, VALUE_LENGTH, 19 | }; 20 | 21 | #[derive(Debug, Default)] 22 | pub struct Stable { 23 | pub body: bool, 24 | pub reflections: bool, 25 | } 26 | 27 | #[derive(Debug, Clone, PartialEq, Eq, Serialize)] 28 | pub enum ReasonKind { 29 | Code, 30 | Text, 31 | Reflected, 32 | NotReflected, 33 | } 34 | 35 | #[derive(Debug, Clone, Serialize)] 36 | pub struct FoundParameter { 37 | pub name: String, 38 | 39 | /// None in case the random parameter name is used 40 | pub value: Option, 41 | 42 | pub diffs: String, 43 | pub status: u16, 44 | pub size: usize, 45 | pub reason_kind: ReasonKind, 46 | } 47 | 48 | impl FoundParameter { 49 | pub fn new>( 50 | name: S, 51 | diffs: &[String], 52 | status: u16, 53 | size: usize, 54 | reason_kind: ReasonKind, 55 | ) -> Self { 56 | let name = name.into(); 57 | 58 | let (name, value) = if name.contains('=') { 59 | let mut name = name.split('='); 60 | ( 61 | name.next().unwrap().to_string(), 62 | Some(name.next().unwrap().to_string()), 63 | ) 64 | } else { 65 | (name, None) 66 | }; 67 | 68 | Self { 69 | name, 70 | value, 71 | diffs: diffs.join("|"), 72 | status, 73 | size, 74 | reason_kind, 75 | } 76 | } 77 | 78 | /// just returns (Key, Value) pair 79 | pub fn get(&self) -> (String, String) { 80 | ( 81 | self.name.clone(), 82 | self.value.clone().unwrap_or_else(|| random_line(VALUE_LENGTH)), 83 | ) 84 | } 85 | 86 | /// returns colored param name and param=value in case a non random value is used 87 | pub fn get_colored(&self) -> String { 88 | let param = match self.reason_kind { 89 | ReasonKind::Code => self.name.yellow(), 90 | ReasonKind::Text => self.name.bright_yellow(), 91 | ReasonKind::Reflected => self.name.bright_blue(), 92 | ReasonKind::NotReflected => self.name.bright_cyan(), 93 | }; 94 | 95 | if self.value.is_some() { 96 | format!("{}={}", param, self.value.as_ref().unwrap()) 97 | } else { 98 | param.to_string() 99 | } 100 | } 101 | } 102 | 103 | pub trait Parameters { 104 | fn contains_name(&self, key: &str) -> bool; 105 | fn contains_name_case_insensitive(&self, key: &str) -> bool; 106 | fn contains_element(&self, el: &FoundParameter) -> bool; 107 | fn contains_element_case_insensitive(&self, el: &FoundParameter) -> bool; 108 | fn process(self, injection_place: InjectionPlace) -> Self; 109 | } 110 | 111 | impl Parameters for Vec { 112 | /// checks whether the element with the same name exists within the vector 113 | fn contains_name(&self, key: &str) -> bool { 114 | self.iter().any(|x| x.name == key) 115 | } 116 | 117 | fn contains_name_case_insensitive(&self, key: &str) -> bool { 118 | self.iter() 119 | .any(|x| x.name.to_lowercase() == key.to_lowercase()) 120 | } 121 | 122 | /// checks whether the combination of name, reason_kind, status exists within the vector 123 | fn contains_element(&self, el: &FoundParameter) -> bool { 124 | self.iter() 125 | .any(|x| x.name == el.name && x.reason_kind == el.reason_kind && x.status == el.status) 126 | } 127 | 128 | fn contains_element_case_insensitive(&self, el: &FoundParameter) -> bool { 129 | self.iter().any(|x| { 130 | x.name.to_lowercase() == el.name.to_lowercase() 131 | && x.reason_kind == el.reason_kind 132 | && x.status == el.status 133 | }) 134 | } 135 | 136 | /// removes duplicates: [debug={random}, Debug={random}, debug=true] -> [debug={random}] 137 | /// not very fast but we are doing it a few times per run anyway 138 | fn process(mut self, injection_place: InjectionPlace) -> Self { 139 | fn capitalize_first(mut x: FoundParameter) -> FoundParameter { 140 | let mut chars = x.name.chars(); 141 | x.name = chars 142 | .next() 143 | .map(|first_letter| first_letter.to_uppercase()) 144 | .into_iter() 145 | .flatten() 146 | .chain(chars) 147 | .collect(); 148 | x 149 | } 150 | 151 | // in case, for example, 'admin' param is found -- remove params like 'admin=true' or sth 152 | self = self 153 | .iter() 154 | .filter(|x| !(x.name.contains('=') && self.contains_element(x))) 155 | .map(|x| x.to_owned()) 156 | .collect(); 157 | 158 | // if there's lowercase alternative - remove that parameter 159 | // so Host & HOST & host are the same parameters and only host should stay 160 | self = self 161 | .iter() 162 | .filter(|x| { 163 | x.name.to_lowercase() == x.name || !self.contains_name(&x.name.to_lowercase()) 164 | }) 165 | .map(|x| x.to_owned()) 166 | .collect(); 167 | 168 | // for now reqwest capitalizes first char of every header 169 | self = if injection_place == InjectionPlace::Headers { 170 | self.iter() 171 | .map(|x| capitalize_first(x.to_owned())) 172 | .collect() 173 | } else { 174 | self 175 | }; 176 | 177 | // if there's HOST and Host only one of them should stay 178 | let mut found_params = vec![]; 179 | for el in self { 180 | if !found_params.contains_name_case_insensitive(&el.name) { 181 | found_params.push(el); 182 | } 183 | } 184 | 185 | found_params 186 | } 187 | } 188 | 189 | /// replays a request with parameters via a different proxy 190 | pub(super) async fn replay<'a>( 191 | config: &Config, 192 | request_defaults: &RequestDefaults, 193 | replay_client: &Client, 194 | found_params: &Vec, 195 | ) -> Result<(), Box> { 196 | 197 | // get cookies 198 | Request::new(request_defaults, vec![]) 199 | .send_by(replay_client) 200 | .await?; 201 | 202 | if config.replay_once { 203 | Request::new( 204 | request_defaults, 205 | found_params 206 | .iter() 207 | .map(|x| x.get()) 208 | .map(|(x, y)| format!("{}={}", x, y)) 209 | .collect::>(), 210 | ) 211 | .send_by(replay_client) 212 | .await?; 213 | } else { 214 | for param in found_params { 215 | let param = param.get(); 216 | Request::new(request_defaults, vec![format!("{}={}", param.0, param.1)]) 217 | .send_by(replay_client) 218 | .await?; 219 | } 220 | } 221 | 222 | Ok(()) 223 | } 224 | 225 | /// verifies found parameters by requesting the page with found parameters yet one time 226 | pub(super) async fn verify<'a>( 227 | initial_response: &'a Response<'a>, 228 | request_defaults: &'a RequestDefaults, 229 | found_params: &Vec, 230 | diffs: &Vec, 231 | stable: &Stable, 232 | ) -> Result, Box> { 233 | let mut filtered_params = Vec::with_capacity(found_params.len()); 234 | 235 | for param in found_params { 236 | let param_value = param.get(); 237 | let mut response = Request::new(request_defaults, vec![format!("{}={}", param_value.0, param_value.1)]) 238 | .send() 239 | .await?; 240 | 241 | let (is_code_diff, new_diffs) = response.compare(initial_response, diffs)?; 242 | let mut is_the_body_the_same = true; 243 | 244 | if !new_diffs.is_empty() { 245 | is_the_body_the_same = false; 246 | } 247 | 248 | response.fill_reflected_parameters(initial_response); 249 | 250 | if is_code_diff || !response.reflected_parameters.is_empty() || stable.body && !is_the_body_the_same { 251 | filtered_params.push(param.clone()); 252 | } 253 | } 254 | 255 | Ok(filtered_params) 256 | } 257 | 258 | pub enum ParamPatterns { 259 | /// _anything 260 | SpecialPrefix(char), 261 | 262 | /// anything1243124 263 | /// from example: (string = anything, usize=7) 264 | HasNumbersPostfix(String, usize), 265 | 266 | /// any!thing 267 | ContainsSpecial(char), 268 | 269 | /// password_anything 270 | BeforeUnderscore(String), 271 | 272 | /// anything_password 273 | AfterUnderscore(String), 274 | 275 | /// password-anything 276 | BeforeDash(String), 277 | 278 | /// anything-password 279 | AfterDash(String), 280 | } 281 | 282 | impl ParamPatterns { 283 | /// returns check parameter to determine whether the prediction is correct 284 | pub fn turn_into_string(self) -> String { 285 | match self { 286 | ParamPatterns::SpecialPrefix(c) => format!("{}anything", c), 287 | ParamPatterns::ContainsSpecial(c) => format!("anyth{}ng", c), 288 | ParamPatterns::BeforeUnderscore(s) => format!("{}_anything", s), 289 | ParamPatterns::AfterUnderscore(s) => format!("anything_{}", s), 290 | ParamPatterns::BeforeDash(s) => format!("{}-anything", s), 291 | ParamPatterns::AfterDash(s) => format!("anything-{}", s), 292 | ParamPatterns::HasNumbersPostfix(s, u) => format!("{}{}", s, "1".repeat(u)), 293 | } 294 | } 295 | 296 | /// in case 2 patterns match like sth1-sth2 == check all the patterns. 297 | /// In case nothing confirms -- leave sth1-sth2 298 | /// In case all confirms ¯\_(ツ)_/¯ 299 | pub fn get_patterns(param: &str) -> Vec { 300 | lazy_static! { 301 | static ref RE_NUMBER_PREFIX: Regex = Regex::new(r"^([^\d]+)(\d+)$").unwrap(); 302 | }; 303 | 304 | let mut patterns = Vec::new(); 305 | let param_chars: Vec = param.chars().collect(); 306 | 307 | if param_chars[0].is_ascii_punctuation() { 308 | patterns.push(ParamPatterns::SpecialPrefix(param_chars[0])) 309 | } 310 | 311 | let special_chars: Vec<&char> = param_chars 312 | .iter() 313 | .filter(|x| x.is_ascii_punctuation() && x != &&'-' && x != &&'_') 314 | .collect(); 315 | if special_chars.len() == 1 { 316 | patterns.push(ParamPatterns::ContainsSpecial(*special_chars[0])); 317 | } 318 | 319 | if param_chars.contains(&'-') { 320 | // we're treating as if there's only one '-' for now. 321 | // maybe needs to be changed in future 322 | let mut splitted = param.split('-'); 323 | 324 | patterns.push(ParamPatterns::BeforeDash( 325 | splitted.next().unwrap().to_string(), 326 | )); 327 | patterns.push(ParamPatterns::AfterDash( 328 | splitted.next().unwrap().to_string(), 329 | )); 330 | } 331 | 332 | if param_chars.contains(&'_') { 333 | // we're treating as if there's only one '_' for now. 334 | // maybe needs to be changed in future 335 | let mut splitted = param.split('_'); 336 | 337 | patterns.push(ParamPatterns::BeforeUnderscore( 338 | splitted.next().unwrap().to_string(), 339 | )); 340 | patterns.push(ParamPatterns::AfterUnderscore( 341 | splitted.next().unwrap().to_string(), 342 | )); 343 | } 344 | 345 | if let Some(caps) = RE_NUMBER_PREFIX.captures(param) { 346 | let (word, digits) = (caps.get(1).unwrap().as_str(), caps.get(2).unwrap().as_str()); 347 | patterns.push(ParamPatterns::HasNumbersPostfix( 348 | word.to_string(), 349 | digits.len(), 350 | )) 351 | } 352 | 353 | patterns 354 | } 355 | } 356 | 357 | /// under development 358 | pub(super) async fn _smart_verify( 359 | initial_response: &Response<'_>, 360 | request_defaults: &RequestDefaults, 361 | found_params: &Vec, 362 | diffs: &Vec, 363 | stable: &Stable, 364 | ) -> Result, Box> { 365 | let mut filtered_params = Vec::with_capacity(found_params.len()); 366 | 367 | for param in found_params { 368 | let _param_patterns = ParamPatterns::get_patterns(¶m.name); 369 | 370 | let mut response = Request::new(request_defaults, vec![param.name.clone()]) 371 | .send() 372 | .await?; 373 | 374 | let (is_code_the_same, new_diffs) = response.compare(initial_response, &diffs)?; 375 | let mut is_the_body_the_same = true; 376 | 377 | if !new_diffs.is_empty() { 378 | is_the_body_the_same = false; 379 | } 380 | 381 | response.fill_reflected_parameters(initial_response); 382 | 383 | if !is_code_the_same || !response.reflected_parameters.is_empty() || stable.body && !is_the_body_the_same { 384 | filtered_params.push(param.clone()); 385 | } 386 | } 387 | 388 | Ok(filtered_params) 389 | } 390 | 391 | /// returns last n chars of an url 392 | pub(super) fn fold_url(url: &str, n: usize) -> String { 393 | if url.len() <= n + 2 { 394 | //we need to add some spaces to align the progress bars 395 | url.to_string() + &" ".repeat(2 + n - url.len()) 396 | } else { 397 | "..".to_owned() + &url[url.len() - n..] 398 | } 399 | } 400 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::File, 3 | io::{self, BufRead, Write}, 4 | path::Path, 5 | }; 6 | 7 | use colored::*; 8 | use indicatif::{MultiProgress, ProgressBar, ProgressStyle, ProgressDrawTarget}; 9 | use linked_hash_map::LinkedHashMap; 10 | use rand::Rng; 11 | use url::Url; 12 | 13 | use crate::{config::structs::Config, RANDOM_CHARSET}; 14 | 15 | pub fn progress_style_learn_requests(config: &Config) -> ProgressStyle { 16 | if config.disable_colors { 17 | ProgressStyle::with_template(&format!("{{prefix}} {{bar:{}}} {{pos:>7}}/{{len:7}}", config.progress_bar_len)) 18 | .unwrap() 19 | .progress_chars("**-") 20 | } else { 21 | ProgressStyle::with_template(&format!("{{prefix}} {{bar:{}.cyan/green}} {{pos:>7}}/{{len:7}}", config.progress_bar_len)) 22 | .unwrap() 23 | .progress_chars("**-") 24 | } 25 | } 26 | 27 | pub fn progress_style_check_requests(config: &Config) -> ProgressStyle { 28 | if config.disable_colors { 29 | ProgressStyle::with_template(&format!("{{prefix}} {{bar:{}}} {{pos:>7}}/{{len:7}}", config.progress_bar_len)) 30 | .unwrap() 31 | .progress_chars("##-") 32 | } else { 33 | ProgressStyle::with_template(&format!("{{prefix}} {{bar:{}.cyan/blue}} {{pos:>7}}/{{len:7}}", config.progress_bar_len)) 34 | .unwrap() 35 | .progress_chars("##-") 36 | } 37 | } 38 | 39 | 40 | /// prints informative messages/non critical errors 41 | pub fn info, T: std::fmt::Display>( 42 | config: &Config, 43 | id: usize, 44 | progress_bar: &ProgressBar, 45 | word: S, 46 | msg: T, 47 | ) { 48 | if config.verbose > 0 { 49 | 50 | let id = if is_id_important(config) { 51 | format!("{} ", color_id(id)) 52 | } else { 53 | String::new() 54 | }; 55 | 56 | let message = format!( 57 | "{}[{}] {}", 58 | id, 59 | word.into().yellow(), 60 | msg 61 | ); 62 | 63 | // in case progress bars are hidden -- the messages from progress_bar.println arent' displayed, so we need to use writeln instead 64 | if config.disable_progress_bar { 65 | writeln!(io::stdout(), "{}", message).ok(); 66 | } else { 67 | progress_bar.println(message); 68 | } 69 | } 70 | } 71 | 72 | /// prints errors. Progress_bar may be null in case the error happened too early (before requests) 73 | pub fn error(msg: T, url: Option<&str>, progress_bar: Option<&ProgressBar>, config: Option<&Config>) { 74 | let message = if url.is_none() { 75 | format!("{} {}", "[#]".red(), msg) 76 | } else { 77 | format!("{} [{}] {}", "[#]".red(), url.unwrap(), msg) 78 | }; 79 | 80 | if progress_bar.is_none() || (config.is_some() && config.unwrap().disable_progress_bar) { 81 | writeln!(io::stdout(), "{}", message).ok(); 82 | } else { 83 | progress_bar.unwrap().println(message); 84 | } 85 | } 86 | 87 | /// initialize progress bars for every url set 88 | pub fn init_progress(config: &Config) -> Vec<(ProgressBar, Vec)> { 89 | let mut urls_to_progress = Vec::new(); 90 | let m = MultiProgress::new(); 91 | 92 | // we're creating an empty progress bar to make one empty line between progress bars and the tool's output 93 | let empty_line = m.add(ProgressBar::new(128)); 94 | let empty_sty = ProgressStyle::with_template(" ").unwrap(); 95 | empty_line.set_style(empty_sty.clone()); 96 | empty_line.inc(1); 97 | urls_to_progress.push((empty_line, vec![String::new()])); 98 | 99 | // in case --one-worker-per-host option is provided -- each url set contains urls with one host 100 | // otherwise it's just url sets with one url 101 | let urls = if config.one_worker_per_host { 102 | order_urls(&config.urls) 103 | } else { 104 | config.urls.iter().map(|x| vec![x.to_owned()]).collect() 105 | }; 106 | 107 | // append progress bars one after another and push them to urls_to_progress 108 | for url_set in urls { 109 | let pb = m.insert_from_back( 110 | 0, 111 | ProgressBar::new(0) 112 | ); 113 | 114 | pb.set_style(empty_sty.clone()); 115 | 116 | if config.disable_progress_bar { 117 | pb.set_draw_target(ProgressDrawTarget::hidden()); 118 | } 119 | 120 | urls_to_progress.push((pb, url_set)); 121 | } 122 | 123 | urls_to_progress 124 | } 125 | 126 | /// read wordlist with parameters 127 | pub fn read_lines

(filename: P) -> io::Result>> 128 | where 129 | P: AsRef, 130 | { 131 | let file = File::open(filename)?; 132 | Ok(io::BufReader::new(file).lines()) 133 | } 134 | 135 | /// read parameters from stdin 136 | pub fn read_stdin_lines() -> Vec { 137 | let stdin = io::stdin(); 138 | stdin.lock().lines().filter_map(|x| x.ok()).collect() 139 | } 140 | 141 | /// generate random word of RANDOM_CHARSET chars 142 | pub fn random_line(size: usize) -> String { 143 | (0..size) 144 | .map(|_| { 145 | let idx = rand::thread_rng().gen_range(0, RANDOM_CHARSET.len()); 146 | RANDOM_CHARSET[idx] as char 147 | }) 148 | .collect() 149 | } 150 | 151 | /// returns colored id when > 1 url is being tested in the same time 152 | pub fn color_id(id: usize) -> String { 153 | if id % 7 == 0 { 154 | id.to_string().white() 155 | } else if id % 6 == 0 { 156 | id.to_string().bright_red() 157 | } else if id % 5 == 0 { 158 | id.to_string().bright_cyan() 159 | } else if id % 4 == 0 { 160 | id.to_string().bright_blue() 161 | } else if id % 3 == 0 { 162 | id.to_string().yellow() 163 | } else if id % 2 == 0 { 164 | id.to_string().bright_green() 165 | } else { 166 | id.to_string().magenta() 167 | }.to_string() 168 | } 169 | 170 | /// moves urls with different hosts to different vectors 171 | pub fn order_urls(urls: &[String]) -> Vec> { 172 | // LinkedHashMap instead of hashmap for preserving the order 173 | // LinkedHashMap> 174 | let mut sorted_urls: LinkedHashMap> = LinkedHashMap::new(); 175 | let mut ordered_urls: Vec> = Vec::new(); 176 | 177 | for url in urls.iter() { 178 | let parsed_url = Url::parse(url).unwrap(); 179 | let host = parsed_url.host_str().unwrap(); 180 | 181 | if sorted_urls.contains_key(host) { 182 | sorted_urls.get_mut(host).unwrap().push(url.to_owned()); 183 | } else { 184 | sorted_urls.insert(host.to_owned(), vec![url.to_owned()]); 185 | } 186 | } 187 | 188 | for host in sorted_urls.clone().keys() { 189 | ordered_urls.push(sorted_urls[host].clone()) 190 | } 191 | 192 | ordered_urls 193 | } 194 | 195 | /// returns true if more than 1 url is being checked a time 196 | pub fn is_id_important(config: &Config) -> bool { 197 | !( 198 | config.workers == 1 || config.urls.len() == 1 || config.verbose == 0 199 | ) 200 | } --------------------------------------------------------------------------------