|[Backdated Post]||Date of finding: 03/08/2020||Actual date of publication: 25/10/2020||[Backdated Post]|
In this post I describe a vulnerability I discovered when practicing on a KU Leuven education/examination platform, E-Systant.
The finding in question was a remote code execution vulnerability, which I was allowed to explore further after initially reporting it to the relevant KU Leuven staff (excellent people btw, you know who you are!). When a friend and I discovered the full implications of the vulnerability (which were rather severe) we were offered a job to fix it. (Thanks again for the opportunity!)
The vulnerability originated from the way commands were passed to a docker container in the back-end in which the command would run.
Once a command was received in the back-end, it would get passed on to a clean docker instance, which hadn’t executed any other command before. The container received its ‘full’ command in the form of a string which would be executed by bash inside of the container. The user-supplied command would be embedded in this ‘full’ command string as one of the parameters. It was possible for this user-supplied command to abuse the lack of separation between data and code in this ‘full’ command string, enabling an attacker to execute arbitrary commands inside of the assigned docker instance and get its results back.
1 2 # Simplified example full_cmd= "cd /tmp/out && runSomeProgram --cmd '" + user_supplied_cmd + "' --out ./result.txt"
There had already been a lot of thought put in to securing this platform. Docker containers would for example be killed after executing a single command, so that no two users could tamper with each other across commands or submissions. We did discover a few flaws which we were able to abuse in order to interfere with other users (through file system & CPU denial-of-service attacks) and read their submissions to the platform. The latter could be rather problematic since E-Systant was available for students during the examination of the course.
Multiple solutions were weighed up against each other, keeping other factors such as performance and future compatibility in mind.
We eventually settled on base64 encoding the user-supplied command and base64 decoding it inside of the docker container. This way the separation between data (the user-supplied command) and code (the rest of the bash command) is maintained throughout execution.
1 2 # Simplified fix full_cmd= "cd /tmp/out && runSomeProgram --cmd '$(echo " + encoded_user_supplied_cmd + " | base64 -d)' --out ./result.txt"
The Real Task
Since E-Systant is a platform made to help its users learn different programming languages, to date including Prolog and Haskell (with python in the pipeline). This means that there will always be a way for someone to execute arbitrary code on the platform, making the RCE fix described above mostly symbolic. As a result we were forced into a way of thinking which one should probably always adopt when doing defensive security work, assume you’ve been breached and still make the best of it. This means we had to assume our docker containers would always be running malicious code and still keep the platform as a whole functional and secure.
To that end, we applied many other significant security and performance upgrades to the E-Systant back-end during our time there. Revamping the docker flow (significantly improving performance and security), bolstering the docker images and making sure the docker-daemon wasn’t running as root (podman ftw!). We prevented other denial-of-service attacks, removed hard-coded credentials, exchanged credentials with API-tokens which had a tighter set of capabilities. We added more auditing to user commands (enabling compression in the database to make this possible with the limited storage capacity available to us) and made sure that a malicious user or attacker would have the most difficult time possible escalating his or her privileges in and outside the docker container by removing unnecessary binaries, tightening file system permissions and interactions while further limiting docker container resources without sacrificing user-experience.