Module: PWN::Plugins::CapabilityBroker::Daemon

Extended by:
FFI::Library
Defined in:
lib/pwn/plugins/capability_broker/daemon.rb

Overview

Linux-only, bounded network capability service. Never acquires privileges.

Class Method Summary collapse

Class Method Details

.authorsObject



249
250
251
# File 'lib/pwn/plugins/capability_broker/daemon.rb', line 249

public_class_method def self.authors
  CapabilityBroker.authors
end

.dispatch(opts = {}) ⇒ Object



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/pwn/plugins/capability_broker/daemon.rb', line 26

public_class_method def self.dispatch(opts = {})
  request = opts[:request]
  raise ArgumentError, 'peer UID denied' unless opts[:peer_uid] == opts[:uid]
  raise ArgumentError, 'object required' unless request.is_a?(Hash)

  operation = request['operation']
  raise ArgumentError, 'operation denied' unless %w[status raw_send capture arp nd].include?(operation)

  interfaces = opts.fetch(:interfaces)
  if operation == 'status'
    caps = File.read('/proc/self/status')[/^CapEff:\s+(\h+)/, 1].to_i(16)
    missing = { 13 => 'CAP_NET_RAW', 12 => 'CAP_NET_ADMIN' }.filter_map { |bit, name| name if caps.nobits?(1 << bit) }
    return { ok: true, backend: 'pwn-capd', missing: missing, interfaces: interfaces }
  end
  raise ArgumentError, 'interface denied' unless interfaces.include?(request['iface'])

  case operation
  when 'raw_send' then raw_send(request: request)
  when 'capture' then capture(request: request)
  when 'arp', 'nd' then neighbors(request: request)
  end
rescue StandardError => e
  { ok: false, degraded: true, error: e.message }
end

.helpObject



253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
# File 'lib/pwn/plugins/capability_broker/daemon.rb', line 253

public_class_method def self.help
  puts "USAGE:
    # Dispatch a typed operation after verifying the configured caller.
    #{self}.dispatch(
      request: 'required - parsed JSON request Hash with string keys',
      peer_uid: 'required - UID from kernel SO_PEERCRED credentials',
      uid: 'required - administrator configured allowed caller UID',
      interfaces: 'required - administrator configured interface allowlist'
    )
    # Handle one bounded request on an accepted Unix connection.
    #{self}.serve_client(
      client: 'required - accepted Unix socket connection',
      uid: 'required - administrator configured allowed caller UID',
      interfaces: 'required - administrator configured interface allowlist',
      timeout: 'optional - total connection deadline in seconds, default 35'
    )
    # Start the Linux listener after narrowing existing capabilities.
    #{self}.run(
      uid: 'required - administrator configured allowed caller UID',
      interfaces: 'required - administrator configured interface allowlist',
      socket: 'optional - protected Unix socket pathname'
    )
    # Parse administrator CLI arguments and run the daemon.
    #{self}.main(argv: 'optional - CLI argument array, default ARGV')
    # Print the module author information.
    #{self}.authors
  "
end

.main(opts = {}) ⇒ Object



230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/pwn/plugins/capability_broker/daemon.rb', line 230

public_class_method def self.main(opts = {})
  config = { interfaces: [] }
  parser = OptionParser.new do |options|
    options.banner = 'Usage: pwn-capd --uid UID --interface IFACE [--socket PATH]'
    options.on('--uid UID', Integer) { |uid| config[:uid] = uid }
    options.on('--interface IFACE') { |iface| config[:interfaces] << iface }
    options.on('--socket PATH') { |path| config[:socket] = path }
  end
  parser.parse!((opts[:argv] || ARGV).dup)
  raise ArgumentError, parser.banner unless config.key?(:uid) && !config[:interfaces].empty?

  previous = Signal.trap('TERM') { raise Interrupt }
  run(config)
rescue Interrupt
  nil
ensure
  Signal.trap('TERM', previous) if previous
end

.run(opts = {}) ⇒ Object



185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/pwn/plugins/capability_broker/daemon.rb', line 185

public_class_method def self.run(opts = {})
  uid = opts[:uid]
  interfaces = opts.fetch(:interfaces)
  path = opts.fetch(:socket, '/run/pwn-capd/control.sock')
  raise ArgumentError, 'UID and at least one interface required' unless uid.is_a?(Integer) && uid >= 0 && interfaces.is_a?(Array) && !interfaces.empty? && interfaces.all? { |iface| iface.is_a?(String) && iface.match?(/\A[a-zA-Z0-9_.:-]{1,15}\z/) }

  state = capability_state
  parent = File.dirname(path)
  FileUtils.mkdir_p(parent, mode: 0o755)
  info = File.stat(parent)
  raise ArgumentError, 'socket directory must be owned by daemon and not group/world writable' unless info.uid == Process.euid && info.mode.nobits?(0o022)

  begin
    old_umask = File.umask(0o177)
    server = UNIXServer.new(path) # Existing paths and symlinks are refused.
    identity = File.lstat(path)
  ensure
    File.umask(old_umask)
  end
  File.chown(uid, -1, path)
  drop_capabilities(state: state)
  server.listen(8)
  loop do
    client = server.accept
    begin
      serve_client(client: client, uid: uid, interfaces: interfaces)
    rescue SystemCallError, IOError, Timeout::Error, ArgumentError
      # A disconnected, malformed or idle client cannot stop the listener.
      nil
    ensure
      client.close
    end
  end
ensure
  server&.close
  if identity
    begin
      current = File.lstat(path)
      File.unlink(path) if current.ino == identity.ino && current.dev == identity.dev
    rescue Errno::ENOENT
      nil
    end
  end
end

.serve_client(opts = {}) ⇒ Object



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/pwn/plugins/capability_broker/daemon.rb', line 147

public_class_method def self.serve_client(opts = {})
  client = opts.fetch(:client)
  Timeout.timeout(opts.fetch(:timeout, 35)) do
    _pid, uid, = client.getsockopt(Socket::SOL_SOCKET, Socket::SO_PEERCRED).unpack('iii')
    begin
      raise ArgumentError, 'peer UID denied' unless uid == opts[:uid]

      line = client.gets(100_001)
      raise ArgumentError, 'request too large or incomplete' unless line && line.bytesize <= 100_000 && line.end_with?("\n")

      response = dispatch(request: JSON.parse(line), peer_uid: uid, uid: opts[:uid], interfaces: opts[:interfaces])
    rescue ArgumentError, JSON::ParserError => e
      response = { ok: false, degraded: true, error: e.message }
    end
    client.write(JSON.generate(response) << "\n")
  end
end