Event | Challenge | Category | Points | Solves |
---|---|---|---|---|
AperiCtf | PwnRunSee 1 | pwn | 175 | 5 |
AperiCtf | PwnRunSee 2 | pwn | 250 | 2 |
TL;DR
This challenge was a use after free vulnerability which allow the user to get a shell on the remote docker after a call to execve with some user controlled parameters. Once inside the docker, we can abuse some privileges to mount the host disk inside the container and get the last flag.
Recon
The program allows the user to create some tickets which will then be processed by some users depending of the ticket destination. If the ticket destination isn’t a valid one a “Intern” is hired to process the ticket then fired.
Agents are stored in a linked list, each agent has a pointer to the next agent and to the previous agent. If there is no previous/next agent it points to NULL. By default, there are 2 agents, the founder and Moss.
1 |
|
After looking at the code we can spot a vulnerability, in the fire_interns function the intern is freed, but the linked list is not updated so the successor of the previous agent (Moss if it’s the first intern) will point to a free memory area. When an object is freed, the object itself is not destroyed and the content of the object remain intact.
1 |
|
So now when we create a new ticket, it will point to the same memory area than the previous freed agent. Here is an example, first you create a useless ticket, then you ask the agents to process the ticket in order to create the Intern agent. Now there are no tickets left and the intern agent is freed. Here is what happen if you create another ticket and display the agent list :
We can control the content of the agent ! It’s time for exploitation.
Building the exploit
Here is the representation of the memory. The red arrows show the memory when the intern has been hired. The purple arrow show the memory when the intern has been freed and when a new ticket is created. The successor pointer of Moss will point to the created ticket and it will be interpreted as an agent. Thanks to that we can control the structure of the agent and put what we want inside.
Now we need to create this special ticket in order to change the intern’s parameters and get him execute a shell on the remote docker. What we want to achieve is a call to execve in the run_taskfunction.
1 |
|
The run_task function is called from process_tickets. It loops over all the tickets and look for a valid agent. When the agent has processed the ticket his ticket counter is increased by one.
1 |
|
But, how the agents are selected in find_agent ? Here is the code responsible for that, first it compares the service of the agent and then check that the ticket count of the agent is lower than MAX_AGENT_TICKET (which is 4 here).
1 |
|
So if we just craft the intern with service ADM it won’t work because Moss will be selected since it takes the agent list from the beginning to the end (Intern is the last one). We need to make Moss busy. The ticket count of Moss need to be bigger than MAX_AGENT_TICKET so he can’t process other tickets, this will make find_agent return the intern.
Before getting access to execve in run_task, some conditions are needed. The agent has to be an admin, the agent can’t be Moss and the ticket count should be greater than 1:
1 |
|
So the idea of the exploit is :
- Create fake ticket for Moss to make him busy
- Create fake ticket for the intern in order to create him
- Process tickets
- Create special ticket to overwrite intern’s params
- Create the shell ticket
- Process the tickets and enjoy your shell !
After overwriting the intern’s params we will end up executing this command :
1 |
|
Exploit
1 |
|
Part 2
So we end up part 1 with a shell, but we need to escape from the container. I’m not an expert in docker, but we can quickly spot the privileged: true in the dockercompose.yml which sound promising !
1 |
|
After some research on duckduckgo I discovered some warning about using this option. From the official docker website :
When the operator executes docker run --privileged, Docker will enable access to all devices on the host as well as set some configuration in AppArmor or SELinux to allow the container nearly all the same access to the host as processes running outside containers on the host.
Indeed we can access all the devices, to see them just use ls /dev
. In the device, there is /dev/sda1 which is the host partition !! To access this partition and escape the container we just need to mount it:
1 |
|
I’m currently using a Mac, with mac and windows systems the docker daemon run in a VM so we can’t really see the host partition, but on a linux machine we would have had access to the whole partition !