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"

source_code.zip

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.

2023-03-30-180704_1881x426_scrot

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.

2023-03-30-180819_1888x1030_scrot

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:

2023-03-30-181052_1667x78_scrot

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:

2023-03-30-181253_251x286_scrot

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:

2023-03-30-181357_1877x516_scrot

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 ?

2023-03-30-181505_287x82_scrot

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

2023-03-30-181624_1196x680_scrot

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 to 0 since the admin’s userId is not 1

2023-03-30-183618_1194x787_scrot

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 !

2023-03-30-183151_1877x668_scrot

It seems like it worked, because the Admin Dashboard looks different, we can now modify users.

Let’s try to read the Flag book !

2023-03-30-183235_279x65_scrot

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:

2023-03-30-183347_1360x353_scrot

2023-03-30-183501_306x65_scrot

Now we can clear our Local Storage so the JWT we created gets refreshed and we should be able to read the Flag

2023-03-30-183813_1888x1030_scrot

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 !

2023-03-28-201238_1888x1030_scrot

2023-03-28-201252_1888x1030_scrot