Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/scapy/layers/msrpce/rpcserver.py: 19%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

189 statements  

1# SPDX-License-Identifier: GPL-2.0-or-later 

2# This file is part of Scapy 

3# See https://scapy.net/ for more information 

4# Copyright (C) Gabriel Potter 

5 

6""" 

7DCE/RPC server as per [MS-RPCE] 

8""" 

9 

10import socket 

11import uuid 

12import threading 

13from collections import deque 

14 

15from scapy.arch import get_if_addr 

16from scapy.config import conf 

17from scapy.data import MTU 

18from scapy.volatile import RandShort 

19 

20from scapy.layers.dcerpc import ( 

21 CommonAuthVerifier, 

22 DCE_RPC_INTERFACES, 

23 DCERPC_Transport, 

24 DceRpc5, 

25 DceRpc5AlterContext, 

26 DceRpc5AlterContextResp, 

27 DceRpc5Auth3, 

28 DceRpc5Bind, 

29 DceRpc5BindAck, 

30 DceRpc5BindNak, 

31 DceRpc5Fault, 

32 DceRpc5PortAny, 

33 DceRpc5Request, 

34 DceRpc5Response, 

35 DceRpc5Result, 

36 DceRpc5TransferSyntax, 

37 DceRpcInterface, 

38 DceRpcSession, 

39 NDRPacket, 

40 RPC_C_AUTHN_LEVEL, 

41) 

42 

43# RPC 

44from scapy.layers.msrpce.ept import ( 

45 ept_map_Request, 

46 ept_map_Response, 

47 twr_p_t, 

48 protocol_tower_t, 

49 prot_and_addr_t, 

50) 

51 

52# Typing 

53from typing import ( 

54 Dict, 

55 Callable, 

56 Optional, 

57 Tuple, 

58) 

59 

60 

61class _DCERPC_Server_metaclass(type): 

62 # This value is calculated for each DCE/RPC server, and contains 

63 # the callables sorted by interface+opnum 

64 dcerpc_commands: Dict[Tuple[uuid.UUID, int], Callable] = {} 

65 

66 def __new__(cls, name, bases, dct): 

67 dct.setdefault( 

68 "dcerpc_commands", 

69 {x.dcerpc_command: x for x in dct.values() if hasattr(x, "dcerpc_command")}, 

70 ) 

71 return type.__new__(cls, name, bases, dct) 

72 

73 

74class DCERPC_Server(metaclass=_DCERPC_Server_metaclass): 

75 def __init__( 

76 self, 

77 transport: DCERPC_Transport, 

78 ndr64: Optional[bool] = None, 

79 verb: bool = True, 

80 local_ip: str = None, 

81 port: int = None, 

82 portmap: Dict[DceRpcInterface, int] = None, 

83 **kwargs, 

84 ): 

85 self.transport = transport 

86 self.session = DceRpcSession(**kwargs) 

87 self.queue = deque() 

88 if ndr64 is None: 

89 ndr64 = conf.ndr64 

90 self.ndr64 = ndr64 

91 # For endpoint mapper. TODO: improve separation/handling of SMB/IP etc 

92 self.local_ip = local_ip 

93 self.port = port 

94 self.portmap = portmap or {} 

95 self.verb = verb 

96 

97 def loop(self, sock): 

98 while True: 

99 pkt = sock.recv(MTU) 

100 if not pkt: 

101 break 

102 self.recv(pkt) 

103 # send all possible responses 

104 while True: 

105 resp = self.get_response() 

106 if not resp: 

107 break 

108 sock.send(bytes(resp)) 

109 

110 @staticmethod 

111 def answer(reqcls): 

112 """ 

113 A decorator that registers a DCE/RPC responder to a command. 

114 See the DCE/RPC documentation. 

115 

116 :param reqcls: the DCE/RPC packet class to respond to 

117 """ 

118 

119 def deco(func): 

120 if not issubclass(reqcls, NDRPacket): 

121 raise ValueError("Cannot answer a non NDRPacket class !") 

122 try: 

123 func.dcerpc_command = reqcls.intf, reqcls.opnum 

124 except AttributeError: 

125 raise ValueError( 

126 "NDRPacket class isn't registered or isn't a request !" 

127 ) 

128 return func 

129 

130 return deco 

131 

132 def extend(self, server_cls): 

133 """ 

134 Extend a DCE/RPC server into another 

135 """ 

136 self.dcerpc_commands.update(server_cls.dcerpc_commands) 

137 

138 def make_reply(self, req): 

139 """ 

140 Make a response to the DCE/RPC request. 

141 

142 This finds whether a callback has been registered for this particular packet, 

143 and call it if available. 

144 """ 

145 opnum = req[DceRpc5Request].opnum 

146 intf = self.session.rpc_bind_interface.uuid 

147 if (intf, opnum) in self.dcerpc_commands: 

148 # call handler 

149 return self.dcerpc_commands[(intf, opnum)](self, req) 

150 return None 

151 

152 @classmethod 

153 def spawn(cls, transport, iface=None, port=135, bg=False, **kwargs): 

154 """ 

155 Spawn a DCE/RPC server 

156 

157 :param transport: one of DCERPC_Transport 

158 :param iface: the interface to spawn it on (default: conf.iface) 

159 :param port: the port to spawn it on (for IP_TCP or the SMB server) 

160 :param bg: background mode? (default: False) 

161 :param ndr64: whether NDR64 is supported or not (default: conf.ndr64). 

162 This attribute will be overwritten if the client doesn't support it. 

163 """ 

164 if transport == DCERPC_Transport.NCACN_IP_TCP: 

165 # IP/TCP case 

166 ssock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 

167 local_ip = get_if_addr(iface or conf.iface) 

168 try: 

169 ssock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 

170 except OSError: 

171 pass 

172 ssock.bind((local_ip, port)) 

173 ssock.listen(5) 

174 sockets = [] 

175 if kwargs.get("verb", True): 

176 print( 

177 conf.color_theme.green( 

178 "Server %s started. Waiting..." % cls.__name__ 

179 ) 

180 ) 

181 

182 def _run(): 

183 # Wait for clients forever 

184 try: 

185 while True: 

186 clientsocket, address = ssock.accept() 

187 sockets.append(clientsocket) 

188 print( 

189 conf.color_theme.gold( 

190 "\u2503 Connection received from %s" % repr(address) 

191 ) 

192 ) 

193 server = cls( 

194 DCERPC_Transport.NCACN_IP_TCP, 

195 local_ip=local_ip, 

196 port=port, 

197 **kwargs, 

198 ) 

199 threading.Thread( 

200 target=server.loop, args=(clientsocket,) 

201 ).start() 

202 except KeyboardInterrupt: 

203 print("X Exiting.") 

204 ssock.shutdown(socket.SHUT_RDWR) 

205 except OSError: 

206 print("X Server closed.") 

207 finally: 

208 for sock in sockets: 

209 try: 

210 sock.shutdown(socket.SHUT_RDWR) 

211 sock.close() 

212 except Exception: 

213 pass 

214 ssock.close() 

215 

216 if bg: 

217 # Background 

218 threading.Thread(target=_run).start() 

219 return ssock 

220 else: 

221 # Non-background 

222 _run() 

223 elif transport == DCERPC_Transport.NCACN_NP: 

224 # SMB case 

225 from scapy.layers.smbserver import SMB_Server 

226 

227 kwargs.setdefault("shares", []) # do not expose files by default 

228 return SMB_Server.spawn( 

229 iface=iface or conf.iface, 

230 port=port, 

231 bg=bg, 

232 # Important: pass the DCE/RPC server 

233 DCERPC_SERVER_CLS=cls, 

234 # SMB parameters 

235 **kwargs, 

236 ) 

237 else: 

238 raise ValueError("Unsupported transport :(") 

239 

240 def recv(self, data): 

241 if isinstance(data, bytes): 

242 req = DceRpc5(data) 

243 else: 

244 req = data 

245 # If the packet has padding, it contains several fragments 

246 pad = None 

247 if conf.padding_layer in req: 

248 pad = req[conf.padding_layer].load 

249 req[conf.padding_layer].underlayer.remove_payload() 

250 # Ask the DCE/RPC session to process it (match interface, etc.) 

251 req = self.session.in_pkt(req) 

252 hdr = DceRpc5( 

253 endian=req.endian, 

254 encoding=req.encoding, 

255 float=req.float, 

256 call_id=req.call_id, 

257 ) 

258 # Now process the packet based on the DCE/RPC type 

259 if DceRpc5Bind in req or DceRpc5AlterContext in req or DceRpc5Auth3 in req: 

260 # Log 

261 if self.verb: 

262 print( 

263 conf.color_theme.opening( 

264 "<< %s" % req.payload.__class__.__name__ 

265 + ( 

266 " (with %s%s)" 

267 % ( 

268 self.session.ssp.__class__.__name__, 

269 ( 

270 f" - {self.session.auth_level.name}" 

271 if self.session.auth_level is not None 

272 else "" 

273 ), 

274 ) 

275 if self.session.ssp 

276 else "" 

277 ) 

278 ) 

279 ) 

280 if not self.session.rpc_bind_interface: 

281 # The session did not find a matching interface ! 

282 self.queue.extend(self.session.out_pkt(hdr / DceRpc5BindNak())) 

283 if self.verb: 

284 print(conf.color_theme.fail("! DceRpc5BindNak (unknown interface)")) 

285 else: 

286 auth_value, status = None, 0 

287 if ( 

288 self.session.ssp 

289 and req.auth_verifier 

290 and req.auth_verifier.auth_value 

291 ): 

292 ( 

293 self.session.sspcontext, 

294 auth_value, 

295 status, 

296 ) = self.session.ssp.GSS_Accept_sec_context( 

297 self.session.sspcontext, req.auth_verifier.auth_value 

298 ) 

299 self.session.auth_level = RPC_C_AUTHN_LEVEL( 

300 req.auth_verifier.auth_level 

301 ) 

302 self.session.auth_context_id = req.auth_verifier.auth_context_id 

303 if DceRpc5Auth3 in req: 

304 # Auth 3 stops here (no server response) ! 

305 if status != 0: 

306 print(conf.color_theme.fail("! DceRpc5Auth3 failed")) 

307 if pad is not None: 

308 self.recv(pad) 

309 return 

310 # auth_verifier here contains the SSP nego packets 

311 # (whereas it usually contains the verifiers) 

312 if auth_value is not None: 

313 hdr.auth_verifier = CommonAuthVerifier( 

314 auth_type=req.auth_verifier.auth_type, 

315 auth_level=req.auth_verifier.auth_level, 

316 auth_context_id=req.auth_verifier.auth_context_id, 

317 auth_value=auth_value, 

318 ) 

319 

320 # Detect if the client requested NDR64 and the server agrees 

321 self.ndr64 = self.ndr64 and any( 

322 ctx.transfer_syntaxes[0].sprintf("%if_uuid%") == "NDR64" 

323 for ctx in req.context_elem 

324 ) 

325 

326 # Process bind contexts and answer to them 

327 results = [] 

328 for ctx in req.context_elem: 

329 # Get name 

330 name = ctx.transfer_syntaxes[0].sprintf("%if_uuid%") 

331 if ( 

332 # NDR64 

333 (name == "NDR64" and self.ndr64) 

334 or 

335 # NDR 2.0 

336 (name == "NDR 2.0" and not self.ndr64) 

337 ): 

338 # Acceptance 

339 results.append( 

340 DceRpc5Result( 

341 result=0, 

342 reason=0, 

343 transfer_syntax=DceRpc5TransferSyntax( 

344 if_uuid=ctx.transfer_syntaxes[0].if_uuid, 

345 if_version=ctx.transfer_syntaxes[0].if_version, 

346 ), 

347 ) 

348 ) 

349 elif name == "Bind Time Feature Negotiation": 

350 # Handle Bind Time Feature 

351 results.append( 

352 DceRpc5Result( 

353 result=3, 

354 reason=3, 

355 transfer_syntax=DceRpc5TransferSyntax( 

356 if_uuid="NULL", 

357 if_version=0, 

358 ), 

359 ) 

360 ) 

361 else: 

362 # Reject 

363 results.append( 

364 DceRpc5Result( 

365 result=2, 

366 reason=2, 

367 transfer_syntax=DceRpc5TransferSyntax( 

368 if_uuid="NULL", 

369 if_version=0, 

370 ), 

371 ) 

372 ) 

373 

374 if self.port is None: 

375 # Piped 

376 port_spec = ( 

377 b"\\\\PIPE\\\\%s\0" 

378 % self.session.rpc_bind_interface.name.encode() 

379 ) 

380 else: 

381 # IP 

382 port_spec = str(self.port).encode() + b"\x00" 

383 if DceRpc5Bind in req: 

384 cls = DceRpc5BindAck 

385 else: 

386 cls = DceRpc5AlterContextResp 

387 self.queue.extend( 

388 self.session.out_pkt( 

389 hdr 

390 / cls( 

391 assoc_group_id=int(RandShort()), 

392 sec_addr=DceRpc5PortAny( 

393 port_spec=port_spec, 

394 ), 

395 results=results, 

396 ), 

397 ) 

398 ) 

399 if self.verb: 

400 print( 

401 conf.color_theme.success( 

402 f">> {cls.__name__} {self.session.rpc_bind_interface.name}" 

403 f" is on port '{port_spec.decode()}' using " 

404 + ("NDR64" if self.ndr64 else "NDR32") 

405 ) 

406 ) 

407 elif DceRpc5Request in req: 

408 if self.verb: 

409 print( 

410 conf.color_theme.opening( 

411 "<< REQUEST: %s" 

412 % req[DceRpc5Request].payload.__class__.__name__ 

413 ) 

414 ) 

415 # Can be any RPC request ! 

416 resp = self.make_reply(req) 

417 if resp: 

418 self.queue.extend( 

419 self.session.out_pkt( 

420 hdr 

421 / DceRpc5Response( 

422 alloc_hint=len(resp), 

423 cont_id=req.cont_id, 

424 ) 

425 / resp, 

426 ) 

427 ) 

428 if self.verb: 

429 print( 

430 conf.color_theme.success( 

431 ">> RESPONSE: %s" % (resp.__class__.__name__) 

432 ) 

433 ) 

434 else: 

435 # Unimplemented request ! 

436 if self.verb: 

437 print( 

438 conf.color_theme.fail( 

439 "! RPC request not implemented by server." 

440 ) 

441 ) 

442 req.show() 

443 

444 # Return a Fault 

445 hdr.pfc_flags += "PFC_DID_NOT_EXECUTE" 

446 self.queue.extend( 

447 hdr 

448 / DceRpc5Fault( 

449 # nca_s_op_rng_error 

450 status=0x1C010002, 

451 cont_id=req.cont_id, 

452 ) 

453 ) 

454 # If there was padding, process the second frag 

455 if pad is not None: 

456 self.recv(pad) 

457 

458 def get_response(self): 

459 try: 

460 return self.queue.popleft() 

461 except IndexError: 

462 return None 

463 

464 # Endpoint mapper 

465 

466 @answer.__func__(ept_map_Request) # hack for Python <= 3.9 

467 def ept_map(self, req): 

468 """ 

469 Answer to ept_map_Request. 

470 """ 

471 if self.transport != DCERPC_Transport.NCACN_IP_TCP: 

472 raise ValueError("Unimplemented") 

473 

474 tower = protocol_tower_t( 

475 req[ept_map_Request].valueof("map_tower.tower_octet_string") 

476 ) 

477 uuid = tower.floors[0].uuid 

478 if_version = (tower.floors[0].rhs << 16) | tower.floors[0].version 

479 

480 # Check for results in our portmap 

481 port = None 

482 if (uuid, if_version) in DCE_RPC_INTERFACES: 

483 interface = DCE_RPC_INTERFACES[(uuid, if_version)] 

484 if interface in self.portmap: 

485 port = self.portmap[interface] 

486 

487 if port is not None: 

488 # Found result 

489 resp_tower = twr_p_t( 

490 tower_octet_string=bytes( 

491 protocol_tower_t( 

492 floors=[ 

493 tower.floors[0], # UUID 

494 tower.floors[1], # NDR version 

495 tower.floors[2], # RPC version 

496 prot_and_addr_t( 

497 lhs_length=1, 

498 protocol_identifier="NCACN_IP_TCP", 

499 rhs_length=2, 

500 rhs=port, 

501 ), 

502 prot_and_addr_t( 

503 lhs_length=1, 

504 protocol_identifier="IP", 

505 rhs_length=4, 

506 rhs=self.local_ip or "0.0.0.0", 

507 ), 

508 ] 

509 ) 

510 ) 

511 ) 

512 resp = ept_map_Response(ITowers=[resp_tower], ndr64=self.ndr64) 

513 resp.ITowers.max_count = req.max_towers # ugh 

514 else: 

515 # No result found 

516 pass 

517 return resp