Java Code Analysis!?!
Overview
- Overall difficulty for me (From 1-10 stars): ★★★☆☆☆☆☆☆☆
Background
Author: Nandan Desai
Description
BookShelf Pico, my premium online book-reading service. I believe that my website is super secure. I challenge you to prove me wrong by reading the ‘Flag’ book! Here are the credentials to get you started:
- Username: “user”
- Password: “user”
Source code can be downloaded here.
Website can be accessed here!.
Enumeration
Home page:
Let’s login with the provided credentials!
In here, we can see there are 3 books!
In this challenge, we’re provided with the source code:
┌[siunam♥earth]-(~/ctf/picoCTF-2023/Web-Exploitation/Java-Code-Analysis!?!)-[2023.03.16|14:08:34(HKT)]
└> file bookshelf-pico.zip
bookshelf-pico.zip: Zip archive data, at least v2.0 to extract, compression method=store
┌[siunam♥earth]-(~/ctf/picoCTF-2023/Web-Exploitation/Java-Code-Analysis!?!)-[2023.03.16|14:08:35(HKT)]
└> unzip bookshelf-pico.zip
Archive: bookshelf-pico.zip
creating: gradle/
creating: gradle/wrapper/
inflating: gradle/wrapper/gradle-wrapper.jar
inflating: gradle/wrapper/gradle-wrapper.properties
[...]
┌[siunam♥earth]-(~/ctf/picoCTF-2023/Web-Exploitation/Java-Code-Analysis!?!)-[2023.03.16|14:12:17(HKT)]
└> ls -lah
total 5.5M
drwxr-xr-x 5 siunam nam 4.0K Mar 16 14:08 .
drwxr-xr-x 3 siunam nam 4.0K Mar 16 14:05 ..
-rw-r--r-- 1 siunam nam 5.5M Mar 16 14:06 bookshelf-pico.zip
-rw-rw-r-- 1 siunam nam 1.5K Jun 7 2022 build.gradle
drwxrwxr-x 3 siunam nam 4.0K Jun 7 2022 gradle
-rwxrwxr-x 1 siunam nam 5.7K Jun 7 2022 gradlew
-rw-rw-r-- 1 siunam nam 2.7K Jun 7 2022 gradlew.bat
-rw-rw-r-- 1 siunam nam 4.5K Dec 9 16:56 README.md
-rw-rw-r-- 1 siunam nam 43 Jun 7 2022 settings.gradle
drwxrwxr-x 4 siunam nam 4.0K Jun 7 2022 src
drwxrwxr-x 4 siunam nam 4.0K Nov 27 07:03 userdata
Burp Suite HTTP history:
When we’re logged in, it’ll response us a JWT (JSON Web Token)!!
Note: It’s highlighted in green because of the “JSON Web Tokens” extension in Burp Suite.
JSON Web Tokens (JWTs) are a standardized format for sending cryptographically signed JSON data between systems. They can theoretically contain any kind of data, but are most commonly used to send information (“claims”) about users as part of authentication, session handling, and access control mechanisms.
Unlike with classic session tokens, all of the data that a server needs is stored client-side within the JWT itself. This makes JWTs a popular choice for highly distributed websites where users need to interact seamlessly with multiple back-end servers.
A JWT consists of 3 parts: a header, a payload, and a signature. These are each separated by a dot.
The header and payload parts of a JWT are just base64url-encoded JSON objects. The header contains metadata about the token itself, while the payload contains the actual “claims” about the user. For example, you can decode the payload from the token above to reveal the following claims from jwt.io:
In the header, we see the algorithm is “HS256”, which is HMAC + SHA-256.
Payload:
{
"role": "Free",
"iss": "bookshelf",
"exp": 1679579451,
"iat": 1678974651,
"userId": 1,
"email": "user"
}
The payload stores some information about the logged in’s user.
Now, what if we sign our own JWT??
However, we didn’t know the secret.
Let’s move on to the source code.
After fumbling around at the source code, I found the following interesting thing.
/security/SecretGenerator.java
:
[...]
@Service
class SecretGenerator {
private Logger logger = LoggerFactory.getLogger(SecretGenerator.class);
private static final String SERVER_SECRET_FILENAME = "server_secret.txt";
@Autowired
private UserDataPaths userDataPaths;
private String generateRandomString(int len) {
// not so random
return "1234";
}
String getServerSecret() {
try {
String secret = new String(FileOperation.readFile(userDataPaths.getCurrentJarPath(), SERVER_SECRET_FILENAME), Charset.defaultCharset());
logger.info("Server secret successfully read from the filesystem. Using the same for this runtime.");
return secret;
}catch (IOException e){
logger.info(SERVER_SECRET_FILENAME+" file doesn't exists or something went wrong in reading that file. Generating a new secret for the server.");
String newSecret = generateRandomString(32);
try {
FileOperation.writeFile(userDataPaths.getCurrentJarPath(), SERVER_SECRET_FILENAME, newSecret.getBytes());
} catch (IOException ex) {
ex.printStackTrace();
}
logger.info("Newly generated secret is now written to the filesystem for persistence.");
return newSecret;
}
}
}
In function generateRandomString()
, we see that it’s returning a string 1234
!
Then, in /security/JwtService.java
, we see how the JWT was created:
[...]
public class JwtService {
private final String SECRET_KEY;
private static final String CLAIM_KEY_USER_ID = "userId";
private static final String CLAIM_KEY_EMAIL = "email";
private static final String CLAIM_KEY_ROLE = "role";
private static final String ISSUER = "bookshelf";
@Autowired
public JwtService(SecretGenerator secretGenerator){
this.SECRET_KEY = secretGenerator.getServerSecret();
}
public String createToken(Integer userId, String email, String role){
Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
Calendar expiration = Calendar.getInstance();
expiration.add(Calendar.DATE, 7); //expires after 7 days
return JWT.create()
.withIssuer(ISSUER)
.withIssuedAt(new Date())
.withExpiresAt(expiration.getTime())
.withClaim(CLAIM_KEY_USER_ID, userId)
.withClaim(CLAIM_KEY_EMAIL, email)
.withClaim(CLAIM_KEY_ROLE, role)
.sign(algorithm);
}
[...]
In here, class JwtService
’s method createToken()
is using the SECRET_KEY
, which is 1234
, to sign the JWT!!!
That being said, we can sign our own evil JWT!!
But before we do that, let’s read 1 more code.
/security/ReauthenticationFilter.java
:
[...]
if (jwtUserInfo != null && SecurityContextHolder.getContext().getAuthentication() == null) {
ArrayList<GrantedAuthority> grantedAuthorities = new ArrayList<>();
grantedAuthorities.add(new UserAuthority(jwtUserInfo.getUserId(), jwtUserInfo.getRole()));
// I trust the user input here :) They'll never be evil, or will they?
UserSecurityDetails userSecurityDetails = new UserSecurityDetails(jwtUserInfo.getEmail(), "", grantedAuthorities);
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userSecurityDetails, token, userSecurityDetails.getAuthorities());
usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
[...]
In here, we see the JWT doesn’t check the userId
and role
is evil or not!!
Also, I found that there are 2 keys in the localStorage:
Hmm… What if I modify the role
key to Admin
??
Wait… I’m admin now???
Let’s go to the “Admin Dashboard”:
Hmm… “Failed to load the users.”
If we go to the Burp Suite HTTP history, we see this request:
“Access is denied”…
With that said, we can’t just modify the token-payload
key to escalate our privilege to admin.
Now, let’s modify our payload’s role
claim, and sign it via secret 1234
!
Then, change the value in auth-token
key in localStorage, and refresh the page:
Boom! We can view all users!!!
In the /base/users
’s response, we see admin’s id
:
[...]
{
"id":2,
"email":"admin",
"fullName":"Admin",
"lastLogin":"2023-03-16T14:08:35.545662851",
"role":"Admin"
}
[...]
Hmm… Let’s change our id (1
) to 2!
Now, can we view the “Flag” book?
We can! And we found the flag!
- Flag:
picoCTF{w34k_jwt_n0t_g00d_7745dc02}
Conclusion
What we’ve learned:
- Privilege Escalation Via Weak JWT Secret