ENOWARS 5 - shatranj
12 July 2021 by Lorenz
shatranj
shatranj is a service, which consists of two parts:
shatranj-server
: backend written in Java which heavily relies on the Spring Framework.shatranj-frontend
: frontend written in NodeJS. We didn’t look further into it.
Backend
Endpoints
The backend implements two unauthenticated endpoints defined in org.shatranj.diwana.shatranjserver.ShatranjServerController
:
- POST
/api/register
: Registers a new user. (Who would have guessed)Body
:username
andpassword
- POST
/api/players
: This endpoint is not implemented at all.
And some authenticated (only accessible by logged-in users) ones (Authentication is done via Basic access Authentication. Username and Password must match a registered user):
-
POST
/api/initialize
: Initialize a new game. (The game is basically chess against a computer. The only difference is you only play with one figure that can move like a queen and a knight and also acts as king)Body
:name
of the game (this is a flag store!)Response
: Empty
-
POST
/api/move
: Perform a move in the current game.Body
:message
parsed in a weird way to a DTO (more on that later). Intended to look like"move": "<from><to>"
, e.g."move": "e1e3"
.Response
: Return value dependent on executed DTO.
-
GET
/api/pastgames
: List past games for current user’s username.Response
: List of information about all past games of current user’s name (including game’s name).
-
GET
/api/replay/{id}
: Gets a replay of a game with givenid
.Response
: List of all moves and game-states (does not include game’s name).
-
POST
/api/strategynote
: Adds a note for the current user.Body
: Content of the note. (Again parsed to a DTO in a weird way. This is a flag store!)Response
: Created note DTO.
-
GET
/api/strategynote/{id}
: Gets a note from the current user with givenid
.Response
: Note DTO
-
GET
/api/Strategynotes
: Gets all notes for the current user.Response
: List of note DTOs
Bug 0: Docker misconfiguration (Exposed Database)
When we look at the docker-compose.yml
, we can see multiple interesting things:
- The mysql database is exposed publicly (
ports
is used instead ofexpose
) - The default root password is
123
Using this knowledge, it is easy to read all flags from the database. (It is also possible to change the database password 😉.)
Exploit
Bug 1: Duplicate usernames + /api/pastgames
This bug is pretty simple, yet we took way to long to find it:
When creating a user, there is no check whether a user with this name already exists:
and /api/pastgames
returns all past games of all users with the same username as ours:
Exploitation
Say a flag-id is abc
. Then user abc
has a flag stored as name of a past game. To get this flag, we register a new user with the same name (abc
) and query /api/pastgames
authorized as the new user to get all past games for username abc
which will include the flag!
Fix
The easiest way to fix this issue is to change /api/pastgames
to match the full user instead of just the username:
Bug 2: Improper DTO de-serialization
Before we look at the bug itself, let’s take a step back and check out the DTOs.
DTOS
There are several DTOs defined:
GameDTO
: Game result.boardState
winner
name
MoveDTO
: Two moves.whiteMove
blackMove
boardStateBefore
MoveReqDTO
: Requested move.move
StrategyNoteDTO
: Note.message
StrategyNoteReqDTO
: Requested Note.id
UserDTO
: User.username
UserRegisterDTO
: Register User.username
password
DTO de-serialization: A right way
When we look at the the /api/register
endpoint, we can see an example for proper DTO parsing:
This will automagically use the Spring framework to correctly parse a UserRegisterDTO
from the Request body.
DTO de-serialization: A wrong way
Now that we saw a right way to do it, let’s look at the /api/move
endpoint:
move
\"
is replaced with"
in our message (quotes are un-escaped).- The resulting string is passed to the
removeQuotes
method which will remove the first and last"
if the message starts and ends with""
. - After this, it is checked, that the message contains
"move":
. (If it does not, the method returns an error.) - A JSON string
input
is created from our message:{
message, "@class":"org.shatranj.diwana.shatranjserver.dto.MoveReqDTO"}
input
is passed tohandleDTOInput
handleDTOInput
-
Input is parsed as json to DTO.
-
Depending on subclass, different actions are chose.
-
UserDTO
: Return ids of all notes for username specified in DTO. StrategyNoteReqDTO
: Return content of Note with id specified in DTO. (This might be a flag.)- (…)
-
As the message is attacker controlled (passed as request body) and the first @class
field determines the actual class during de-serialization, we can modify the JSON string, such that input
will no longer be a serialized MoveReqDTO
, but a DTO
of our choosing.
To exploit this, we first craft a message that will be de-serialized into UserDTO
(username is a flag-id again). From this, we will get the ids of all notes for this user. Then we can craft a StrategyNoteReqDTO
to read the content of this note.
Exploit
Fix
The proper way to fix this issue would be handling the input the same way as the /api/register
endpoint does.
However, this is our CTF-grade fix: