-
[Study] ASIS CTF 2021 ASCII art as a service WriteupCTF/ASIS CTF 2021 2021. 10. 26. 00:53
주어진 페이지에 접근해보면, 이미지를 아스키로 바꿔주는 서비스가 제공된다.
아스키 결과물 원본 이미지 주어진 소스를 분석해보자.
#!/usr/bin/env node const express = require('express') const childProcess = require('child_process') const expressSession = require('express-session') const fs = require('fs') const crypto = require('crypto') const app = express() const flag = process.env.FLAG || process.exit() const genRequestToken = () => Array(32).fill().map(()=>"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".charAt(Math.random()*62)).join("") app.use(express.static("./static")) app.use(expressSession({ secret: crypto.randomBytes(32).toString("base64"), resave: false, saveUninitialized: true, cookie: { secure: false, sameSite: 'Lax' } })) app.use(express.json()) app.post('/request',(req,res)=>{ const url = req.body.url const reqToken = genRequestToken() const reqFileName = `./request/${reqToken}` const outputFileName = `./output/${genRequestToken()}` fs.writeFileSync(reqFileName,[reqToken,req.session.id,"Processing..."].join('|')) setTimeout(()=>{ try{ const output = childProcess.execFileSync("timeout",["2","jp2a",...url]) ##### ................................................................. (1) fs.writeFileSync(outputFileName,output.toString()) fs.writeFileSync(reqFileName,[reqToken,req.session.id,outputFileName].join('|')) } catch(e){ fs.writeFileSync(reqFileName,[reqToken,req.session.id,"Something bad happened!"].join('|')) } },2000) res.redirect(`/request/${reqToken}`) }) app.get("/request/:reqtoken",(req,res)=>{ const reqToken = req.params.reqtoken const reqFilename = `./request/${reqToken}` var content if(!/^[a-zA-Z0-9]{32}$/.test(reqToken) || !fs.existsSync(reqFilename)) return res.json( { failed: true, result: "bad request token." }) const [origReqToken,ownerSessid,result] = fs.readFileSync(reqFilename).toString().split("|") ##### ........................................................................ (2) if(req.session.id != ownerSessid) return res.json( { failed: true, result: "Permissions..." }) ##### ........................................................................ (3) if(result[0] != ".") return res.json( { failed: true, result: result }) try{ content = fs.readFileSync(result).toString(); } catch(e) { return res.json({ failed: false, result: "Something bad happened!" }) } res.json({ failed: false, result: content }) res.end() }) app.get("/flag",(req,res)=>{ if(req.ip == "127.0.0.1" || req.ip == "::ffff:127.0.0.1") res.json({ failed: false, result: flag }) else res.json({ failed: true, result: "Flag is not yours..." }) }) function clearOutput(){ try{ childProcess.execSync("rm ./output/* ./request/* 2> /dev/null") } catch(e){} setTimeout(clearOutput,120e3) } clearOutput() app.listen(1357)
전체적인 로직은 아래와 같다.
사용자로부터 URL 입력 받음 ->
입력받은 URL을 인자로 하여 jp2a 명령어 실행(결과값 아스키 코드로 이루어진 이미지) ->
./request/ 디렉토리에 랜덤값을 이름으로 하여 파일 작성 ->
해당 파일을 읽어서 결과값 출력
시도 1) 처음에는 플래그의 출력 조건이 서버 스스로 /flag를 호출하는 것이어서 XSS쪽으로 방향을 잡아봤었는데, 임의의 아스키 값인 jp2a 결과 값으로는 스크립트를 실행 시킬 수 없었다.
시도 2) 사용자 입력 값으로 URL 대신 --version 등 명령어가 바로 종료되는 옵션을 사용후 Command Injection.
로컬에서 환경 구축 후 위와 같이 시도해보았다.
로컬에서 디버깅 결과 커맨드 인젝션에는 실패했다. 하지만, 이 과정을 통해서 옵션을 부여 할 수 있다는 힌트를 얻었다.
활용할만한 옵션이 있는지 확인하기 위해 jp2a에 어떤 옵션이 있나 파악해보았다.
흥미로운 옵션들을 발견했는데,
--html 옵션을 통해 결과파일을 html 형식으로 지정 할 수 있었고
--html-title=.. 옵션을 통해 해당 html 파일의 타이틀을 작성 할 수 있었다.
또한, --output=... 옵션을 통해 결과파일의 경로와 이름을 지정 할 수 있었다.
위 기능을 활용하여 문제를 해결하기 위해 소스코드를 보다 자세히 분석하여 시나리오를 세웠다.
중요한 부분은 세가지다.
(1) 사용자 입력 값 URL을 입력 받아서 jp2a [URL] command가 수행된다.
-> 옵션 부여 가능 확인. html 형식의 output으로 원하는 경로/이름[ex) ./request/aaa~]을 지정하여 저장 할 수 있다.
(2) 결과 값을 호출하는 과정에서 | 를 기준으로 데이터를 구분한다.
-> 기본 오리 이미지에서는 | 가 출력되지 않기 때문에 title을 작성하는 부분에서 | 를 활용하여 ownerSessid와 result를 지정 할 수 있다.
(3) 요청 세션 ID 와 ownerSessid(작성자의세션) 이 일치해야 해당 파일을 읽을 수 있다.
-> 디버깅 결과 req.session.id 는 쿠키의 connect.sid 값에서 "s%3A"와 "."의 사이 값임을 알 수 있다. (express session)
위 시나리오를 바탕으로 아래와 같이 ./request/aaa~~ 라는 파일을 생성하였다.
title이 " |[SESSION값]|../../../../../../../proc/self/environ| " 인 html 페이지가 생성될 것이고,
| 로 구분되어 최종적으로 result에는 FLAG가 선언되어있는 environ파일의 절대경로가 지정되어 해당 파일을 반환할것이다.
실제로, 입력한 [SESSION값]을 맞추어 생성한 파일에 접근하면 플래그를 획득 할 수 있다.
FLAG = ASIS{ascii_art_is_the_real_art_o/_a39bc8}
반응형