前言
继续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