CTF/ASIS CTF 2021

[Study] ASIS CTF 2021 ASCII art as a service Writeup

Vardy 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}

반응형