7 minutes
🇬🇧 PicoCTF 2023 - web/Java Code Analysis!?!
Note
Java Code Analysis!?! was a web exploitation challenge from PicoCTF 2023.
I have done a bunch of JWT challenges, especially on https://root-me.org and I’m starting to like exploiting these tokens. Anyways, it was a fun challenge with some basic code analysis and JWT exploiting.
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"
Recon
When we go to the website, we see this login form and we can also create an account, but we won’t need to because we’re given credentials in the challenge description.
Once we’re logged in, we get redirected to the website’s main page. There are three books, a free one, a premium one and an admin one which is the flag.
Alright, let’s take a look around the website and its source code to see if we can find any interesting stuff…
In the local storage, we find an auth-token
and a token-payload
keys:
The token-payload
looks like it specifies our role, which is supposedly the value we should change to become admin, let’s try to change it manually and refresh the page:
At the top right corner of the page, the “Free” became “Admin” and we now have access to the Admin Dashboard, let’s take a look at this:
It seems we can delete the books, but that’s not what we want to do, we only need to read the Flag book… maybe we can now read it since we’re an admin ?
Of course not ! That would have been way too easy… Time to take a look at this jwt:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoiRnJlZSIsImlzcyI6ImJvb2tzaGVsZiIsImV4c
CI6MTY4MDc5NzI5NCwiaWF0IjoxNjgwMTkyNDk0LCJ1c2VySWQiOjEsImVtYWlsIjoidXNlciJ9.LsB8gkkevf
0hJiLP6HOTdzvakaT_yRXofg3mH_gTJls
Exploitation
By pasting it into https://jwt.io, we learn a little bit more about what is stored in this token. It seems like the token-payload
actually corresponds to the JWT
’s payload (obviously).
Alright, so we can assume we also need to modify the JWT
to modify our role..
I’ve tried the basic exploits such as None alg and cracking the but none of them worked
Time to take a look at the source code !
.
├── build.gradle
├── gradle
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── README.md
├── settings.gradle
├── src
│ ├── main
│ │ ├── java
│ │ │ └── io
│ │ │ └── github
│ │ │ └── nandandesai
│ │ │ └── pico
│ │ │ ├── BookShelfBaseServerApplication.java
│ │ │ ├── configs
│ │ │ │ ├── BookShelfConfig.java
│ │ │ │ └── UserDataPaths.java
│ │ │ ├── controllers
│ │ │ │ ├── BookController.java
│ │ │ │ ├── GeneralErrorController.java
│ │ │ │ └── UserController.java
│ │ │ ├── dto
│ │ │ │ ├── BookDto.java
│ │ │ │ ├── PDF.java
│ │ │ │ ├── Photo.java
│ │ │ │ ├── requests
│ │ │ │ │ ├── AddBookRequest.java
│ │ │ │ │ ├── AddUserPhotoRequest.java
│ │ │ │ │ ├── UpdateUserPasswordRequest.java
│ │ │ │ │ ├── UpdateUserRoleRequest.java
│ │ │ │ │ ├── UserLoginRequest.java
│ │ │ │ │ └── UserSignUpRequest.java
│ │ │ │ ├── responses
│ │ │ │ │ ├── ErrorResponse.java
│ │ │ │ │ ├── Response.java
│ │ │ │ │ └── ResponseType.java
│ │ │ │ ├── RoleDto.java
│ │ │ │ └── UserDto.java
│ │ │ ├── exceptions
│ │ │ │ ├── CommonExceptionHandler.java
│ │ │ │ ├── DuplicateEntityException.java
│ │ │ │ ├── InternalServerException.java
│ │ │ │ ├── LoginFailedException.java
│ │ │ │ ├── ResourceNotFoundException.java
│ │ │ │ ├── ValidationExceptionHandler.java
│ │ │ │ └── ValidationException.java
│ │ │ ├── models
│ │ │ │ ├── Book.java
│ │ │ │ ├── Role.java
│ │ │ │ └── User.java
│ │ │ ├── repositories
│ │ │ │ ├── BookRepository.java
│ │ │ │ ├── RoleRepository.java
│ │ │ │ └── UserRepository.java
│ │ │ ├── security
│ │ │ │ ├── BookPdfAccessCheck.java
│ │ │ │ ├── JwtService.java
│ │ │ │ ├── models
│ │ │ │ │ ├── JwtUserInfo.java
│ │ │ │ │ ├── UserAuthority.java
│ │ │ │ │ └── UserSecurityDetails.java
│ │ │ │ ├── ReauthenticationFilter.java
│ │ │ │ ├── SecretGenerator.java
│ │ │ │ ├── SecurityConfig.java
│ │ │ │ ├── TokenException.java
│ │ │ │ └── UserSecurityDetailsService.java
│ │ │ ├── services
│ │ │ │ ├── BookService.java
│ │ │ │ └── UserService.java
│ │ │ ├── utils
│ │ │ │ └── FileOperation.java
│ │ │ └── validators
│ │ │ ├── ImageConstraintValidator.java
│ │ │ ├── PasswordConstraintValidator.java
│ │ │ ├── PathConstraintValidator.java
│ │ │ ├── PdfConstraintValidator.java
│ │ │ ├── ValidImage.java
│ │ │ ├── ValidPassword.java
│ │ │ ├── ValidPath.java
│ │ │ └── ValidPdf.java
│ │ └── resources
│ │ ├── application.properties
│ │ ├── data.sql
│ │ ├── public
│ │ │ ├── 3rdpartylicenses.txt
│ │ │ ├── assets
│ │ │ │ ├── logo.png
│ │ │ │ └── pdf.worker.es5.v2.5.207.js
│ │ │ ├── favicon.ico
│ │ │ ├── index.html
│ │ │ ├── main.e4577fe86905373d4f38.js
│ │ │ ├── <...>
│ │ │ ├── runtime.acf0dec4155e77772545.js
│ │ │ └── styles.8170aebc9f7ad1381e97.css
│ │ └── static
│ │ └── error.html
│ └── <...>
└── <...>
37 directories, 109 files
That’s a lot of files, but it seems like the only useful directory here is src/main/java/io/github/nandandesai/pico
.
In the security
directory, we see two interesting files: JwtService.java
and SecretGenerator.java
.
Let’s take a look at them, first, JwtService.java
:
package io.github.nandandesai.pico.security;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import io.github.nandandesai.pico.security.models.JwtUserInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Calendar;
import java.util.Date;
@Service
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 = "1234"
}
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);
}
public JwtUserInfo decodeToken(String token) throws JWTVerificationException {
Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer(ISSUER)
.build();
DecodedJWT jwt = verifier.verify(token);
Integer userId = jwt.getClaim(CLAIM_KEY_USER_ID).asInt();
String email = jwt.getClaim(CLAIM_KEY_EMAIL).asString();
String role = jwt.getClaim(CLAIM_KEY_ROLE).asString();
return new JwtUserInfo().setEmail(email)
.setRole(role)
.setUserId(userId);
}
}
One line is especially important:
public JwtService(SecretGenerator secretGenerator){
this.SECRET_KEY = "1234"
}
This is the key used to sign all the generated JWTs
, with this, we can create and modify JWT
as we want, and thus we can modify our role on the website.
Strangely the key is also visible in the
SecretGenerator.java
file:
package io.github.nandandesai.pico.security;
import io.github.nandandesai.pico.configs.UserDataPaths;
import io.github.nandandesai.pico.utils.FileOperation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.nio.charset.Charset;
@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;
}
}
}
private String generateRandomString(int len) {
// not so random
return "1234";
}
Now that we have the key, we can modify our token with the admin role and sign it with the key 1234.
Be careful, you should also change the
userId
field to0
since the admin’suserId
is not1
We can now copy the new token and modify the auth-token
key value with it, then refresh the page and we should be admin !
It seems like it worked, because the Admin Dashboard looks different, we can now modify users.
Let’s try to read the Flag book !
Whaaat? Why?
I don’t know, but we can try to modify the “User” user’s permissions to Admin and see if he can read the book:
Now we can clear our Local Storage so the JWT we created gets refreshed and we should be able to read the Flag…
And it worked ! GGs !
CTF Conclusion
That writeup will be my last one for this edition of the CTF (because I don’t think making a writeup on the other challenges I’ve flagged is useful)
This was a very fun CTF - even though I would have enjoyed more pwn
challenges :c
Me and my team ended up 404/6924 which is great ! Thanks to all my mates and to the organizers, see you next year !