plaidctf2021部分wp

Pearl’s U-Stor

首页就是一个文件上传功能,可以上传任意文件

上传之后,页面会回显你上传的文件名,并且无法上传同名文件,也就是无法覆盖已上传的文件

看到这里,我第一个想到的是有可能是后缀名的ssti

然后我进行尝试,发现{}会被去掉,所以这个思路算是暂时行不通了

回头看看题目给的源码文件,主要逻辑如下,备注是我的分析过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
from flask import Flask, render_template, url_for, request, send_from_directory, send_file, make_response, abort, redirect
from forms import AppFileForm
import os
from io import BytesIO
from werkzeug.utils import secure_filename
from subprocess import Popen
import uuid
import sys
from paste.translogger import TransLogger
import waitress
import time
from flask_wtf.csrf import CSRFProtect

app = Flask(__name__)
app.config['SECRET_KEY'] = "adfadfafasdfasd"
app.config['UPLOAD_FOLDER'] = 'uploads'
app.config['TMP_FOLDER'] = r"C:\Users\\13391\\Desktop\\app\\tmp"
app.config['RECAPTCHA_DATA_ATTRS'] = {'bind': 'recaptcha-submit', 'callback': 'onSubmitCallback', 'size': 'invisible'}
app.config['RECAPTCHA_PUBLIC_KEY'] = os.environ.get("APP_RECAPTCHA_PUBLIC_KEY")
app.config['RECAPTCHA_PRIVATE_KEY'] = os.environ.get("APP_RECAPTCHA_PRIVATE_KEY")
csrf = CSRFProtect(app)

def get_cookie(cookies):
if 'id' in cookies:
cookie_id = cookies.get('id')

if cookie_id.strip() != '' and os.path.exists(os.path.join(app.config['TMP_FOLDER'], cookie_id)):
return (False, cookie_id)

cookie_id = uuid.uuid4().hex
os.mkdir(os.path.join(app.config['TMP_FOLDER'], cookie_id))
return (True,cookie_id)


@app.route('/', methods=["GET", "POST"])
def index():
#cookieid是临时上传文件夹,如果id存在且文件夹存在,则返回cookie id,否则重新生成一个id和对应文件夹
#这里可能存在目录穿越漏洞,而且穿越的必须是已存在的文件夹
#这个地方说不定能目录穿越然后列文件下载?
(set_cookie, cookie_id) = get_cookie(request.cookies)

form = AppFileForm()
if request.method == "GET":
try:
file_list = os.listdir(os.path.join(app.config['TMP_FOLDER'], cookie_id))
except PermissionError:
abort(404, description="Nothing here.")
#使用了模板渲染,没有二次渲染漏洞
resp = make_response(render_template("index.html", form=form, files=file_list))
elif request.method == "POST":
errors = []
#这里应该是在验证csrf_token
if form.validate_on_submit():
myfile = request.files["myfile"]
#这里对cookie_id进行了过滤,也对secure_filename进行了过滤
file_path = os.path.join(app.config['TMP_FOLDER'], secure_filename(cookie_id), secure_filename(myfile.filename))
if os.path.exists(file_path):
errors.append("File already exists.")
#cookieid不能为空
elif secure_filename(cookie_id) == '':
errors.append("Cannot store file.")
else:
try:
myfile.save(file_path)
cmd = ["chattr", "+r", file_path]
proc = Popen(cmd, stdin=None, stderr=None, close_fds=True)
except:
errors.append("Cannot store file.")

try:
file_list = os.listdir(os.path.join(app.config['TMP_FOLDER'], cookie_id))
except PermissionError:
abort(404, description="Nothing here.")
resp = make_response(render_template("index.html", form=form, files=file_list, errors=errors))

if set_cookie:
resp.set_cookie('id', cookie_id)
return resp

#发送上传的文件
@app.route('/file/<path:filename>')
def get_file(filename):
(set_cookie, cookie_id) = get_cookie(request.cookies)
filename = secure_filename(filename)

if set_cookie:
abort(404, description="Nothing here.")
path = os.path.join(app.config['TMP_FOLDER'], secure_filename(cookie_id), filename)
if not os.path.exists(os.path.join(app.config['TMP_FOLDER'], secure_filename(cookie_id), filename)):
abort(404, description="Nothing here.")

with open(os.path.join(app.config['TMP_FOLDER'], secure_filename(cookie_id), filename), "rb") as f:
memory_file = f.read()
return send_file(BytesIO(memory_file), attachment_filename=filename, as_attachment=True)


@app.errorhandler(404)
def page_not_found(error):
return render_template("error.html", message=error)



class AppLogger(TransLogger):
def write_log(self, environ, method, req_uri, start, status, bytes):
if method == 'POST' and 'myfile' in environ['werkzeug.request'].files:
filename = environ['werkzeug.request'].files["myfile"].filename
else:
filename = ''

if bytes is None:
bytes = '-'
remote_addr = '-'
if environ.get('HTTP_X_FORWARDED_FOR'):
remote_addr = environ['HTTP_X_FORWARDED_FOR']
elif environ.get('REMOTE_ADDR'):
remote_addr = environ['REMOTE_ADDR']
d = {
'REMOTE_ADDR': remote_addr,
'REMOTE_USER': environ.get('REMOTE_USER') or '-',
'REQUEST_METHOD': method,
'REQUEST_URI': req_uri,
'HTTP_VERSION': environ.get('SERVER_PROTOCOL'),
'time': time.strftime('%d/%b/%Y:%H:%M:%S', start),
'status': status.split(None, 1)[0],
'bytes': bytes,
'HTTP_REFERER': environ.get('HTTP_REFERER', '-'),
'HTTP_USER_AGENT': environ.get('HTTP_USER_AGENT', '-'),
'ID': environ['werkzeug.request'].cookies['id'] if 'id' in environ['werkzeug.request'].cookies else '',
'FILENAME': filename
}
message = self.format % d
self.logger.log(self.logging_level, message)

if __name__ == "__main__":
format_logger = ('%(REMOTE_ADDR)s - %(REMOTE_USER)s [%(time)s] '
'"%(REQUEST_METHOD)s %(REQUEST_URI)s %(HTTP_VERSION)s" '
'%(status)s %(bytes)s "%(ID)s" "%(FILENAME)s"')
waitress.serve(AppLogger(app, format=format_logger), listen="*:8000")

咱们挑重点的来看,一共有两处

第一,get_file功能

1
file_path = os.path.join(app.config['TMP_FOLDER'], secure_filename(cookie_id), secure_filename(myfile.filename))

这个是在get_file,也就是下载文件功能里的,对cookie_id和filename进行了过滤,我将过滤函数给抠出来,然后进行测试,看看过滤了哪些字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import codecs
import os
import pkgutil
import re
import sys
from werkzeug._compat import text_type




def secure_filename(filename):
PY2 = sys.version_info[0] == 2

_windows_device_files = (
"CON",
"AUX",
"COM1",
"COM2",
"COM3",
"COM4",
"LPT1",
"LPT2",
"LPT3",
"PRN",
"NUL",
)
_filename_ascii_strip_re = re.compile(r"[^A-Za-z0-9_.-]")
if isinstance(filename, text_type):
from unicodedata import normalize

filename = normalize("NFKD", filename).encode("ascii", "ignore")
if not PY2:
filename = filename.decode("ascii")
for sep in os.path.sep, os.path.altsep:
if sep:
filename = filename.replace(sep, " ")
filename = str(_filename_ascii_strip_re.sub("", "_".join(filename.split()))).strip(
"._"
) #移除字符串头尾指定的序列(._)


if (
os.name == "nt"
and filename
and filename.split(".")[0].upper() in _windows_device_files
):
filename = "_" + filename

return filename



filename = "{1-1}"
res = secure_filename(filename)
print(res)

可以看到,它只允许文件名由A-Za-z0-9_.-组成,除此之外的字符都会被去掉,所以在文件上传功能中要进行目录穿越,大概率是行不通的(至少我没想到解决办法),所以尝试将眼光转向别处

第二,列目录功能

就是文件上传后,展示上传文件的功能

从以下代码中能够惊喜的发现,这里对cookie_id的值没有任何过滤,也就是说我们可以通过更改cookie_id,来实现目录穿越列文件

1
file_list = os.listdir(os.path.join(app.config['TMP_FOLDER'], cookie_id))

成功!

获取flag绝对路径

既然我们可以列目录,那不妨先找一找flag在哪里

通过不懈尝试,最终发现了/cygdrive/c/flag.txt

迫不及待点击下载,搓搓手等flag出现,发现获取失败

突然回想起来get_file处对cookie_id进行过滤了,也就是说,无法目录穿越拿文件了

获取flag.txt

目前,我们已有flag的绝对路径,但是无法目录穿越获取,我第一反应是利用linux的软链接

先本地建立软链接: flag-> /cygdrive/c/flag.txt ,然后上传这个flag,然后重新下载,说不定就能拿到真正的flag了

但是我发现,用我的ubuntu虚拟机根本无法上传,上传任何文件都会返回错误,就无法尝试这样是否能成功,回想一下之前的细节,注意到了一个目录特征

还记得它吗,这个就是我们在便利目录时的截图,可以看到,根目录下有一个Cygwin-Terminal.ico的图标文件,经过查询之后,发现Cygwin是一个可以允许在windows下的linux虚拟机,那么正确的思路会不会和它有关呢

通过查阅资料,我们获得以下信息

https://gist.github.com/karlding/7868aac0c54fe94b7ba0a2061e0a4939/

我们可以通过命令

1
2
export GYMWIN=winsymlinks:lnk
ln -s /cygdrive/c/flag.txt ./flag

来创建一个GYMWIN的专属快捷方式flag.lnk,然后上传这个快捷方式,再下载就能得到flag了


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!