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

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

176 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 threading 

12from collections import deque 

13 

14from scapy.arch import get_if_addr 

15from scapy.config import conf 

16from scapy.data import MTU 

17from scapy.volatile import RandShort 

18 

19from scapy.layers.dcerpc import ( 

20 CommonAuthVerifier, 

21 DCE_RPC_INTERFACES, 

22 DCERPC_Transport, 

23 DceRpc5, 

24 DceRpc5AlterContext, 

25 DceRpc5AlterContextResp, 

26 DceRpc5Auth3, 

27 DceRpc5Bind, 

28 DceRpc5BindAck, 

29 DceRpc5BindNak, 

30 DceRpc5PortAny, 

31 DceRpc5Request, 

32 DceRpc5Response, 

33 DceRpc5Result, 

34 DceRpc5TransferSyntax, 

35 DceRpcInterface, 

36 DceRpcSession, 

37 RPC_C_AUTHN_LEVEL, 

38) 

39 

40# RPC 

41from scapy.layers.msrpce.ept import ( 

42 ept_map_Request, 

43 ept_map_Response, 

44 twr_p_t, 

45 protocol_tower_t, 

46 prot_and_addr_t, 

47) 

48 

49# Typing 

50from typing import ( 

51 Dict, 

52 Optional, 

53) 

54 

55 

56class _DCERPC_Server_metaclass(type): 

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

58 dct.setdefault( 

59 "dcerpc_commands", 

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

61 ) 

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

63 

64 

65class DCERPC_Server(metaclass=_DCERPC_Server_metaclass): 

66 def __init__( 

67 self, 

68 transport: DCERPC_Transport, 

69 ndr64: Optional[bool] = None, 

70 verb: bool = True, 

71 local_ip: str = None, 

72 port: int = None, 

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

74 **kwargs, 

75 ): 

76 self.transport = transport 

77 self.session = DceRpcSession(**kwargs) 

78 self.queue = deque() 

79 if ndr64 is None: 

80 ndr64 = conf.ndr64 

81 self.ndr64 = ndr64 

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

83 self.local_ip = local_ip 

84 self.port = port 

85 self.portmap = portmap or {} 

86 self.verb = verb 

87 

88 def loop(self, sock): 

89 while True: 

90 pkt = sock.recv(MTU) 

91 if not pkt: 

92 break 

93 self.recv(pkt) 

94 # send all possible responses 

95 while True: 

96 resp = self.get_response() 

97 if not resp: 

98 break 

99 sock.send(bytes(resp)) 

100 

101 @staticmethod 

102 def answer(reqcls): 

103 """ 

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

105 See the DCE/RPC documentation. 

106 

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

108 """ 

109 

110 def deco(func): 

111 func.dcerpc_command = reqcls 

112 return func 

113 

114 return deco 

115 

116 def extend(self, server_cls): 

117 """ 

118 Extend a DCE/RPC server into another 

119 """ 

120 self.dcerpc_commands.update(server_cls.dcerpc_commands) 

121 

122 def make_reply(self, req): 

123 cls = req[DceRpc5Request].payload.__class__ 

124 if cls in self.dcerpc_commands: 

125 # call handler 

126 return self.dcerpc_commands[cls](self, req) 

127 return None 

128 

129 @classmethod 

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

131 """ 

132 Spawn a DCE/RPC server 

133 

134 :param transport: one of DCERPC_Transport 

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

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

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

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

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

140 """ 

141 if transport == DCERPC_Transport.NCACN_IP_TCP: 

142 # IP/TCP case 

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

144 local_ip = get_if_addr(iface or conf.iface) 

145 try: 

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

147 except OSError: 

148 pass 

149 ssock.bind((local_ip, port)) 

150 ssock.listen(5) 

151 sockets = [] 

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

153 print( 

154 conf.color_theme.green( 

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

156 ) 

157 ) 

158 

159 def _run(): 

160 # Wait for clients forever 

161 try: 

162 while True: 

163 clientsocket, address = ssock.accept() 

164 sockets.append(clientsocket) 

165 print( 

166 conf.color_theme.gold( 

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

168 ) 

169 ) 

170 server = cls( 

171 DCERPC_Transport.NCACN_IP_TCP, 

172 local_ip=local_ip, 

173 port=port, 

174 **kwargs, 

175 ) 

176 threading.Thread( 

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

178 ).start() 

179 except KeyboardInterrupt: 

180 print("X Exiting.") 

181 ssock.shutdown(socket.SHUT_RDWR) 

182 except OSError: 

183 print("X Server closed.") 

184 finally: 

185 for sock in sockets: 

186 try: 

187 sock.shutdown(socket.SHUT_RDWR) 

188 sock.close() 

189 except Exception: 

190 pass 

191 ssock.close() 

192 

193 if bg: 

194 # Background 

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

196 return ssock 

197 else: 

198 # Non-background 

199 _run() 

200 elif transport == DCERPC_Transport.NCACN_NP: 

201 # SMB case 

202 from scapy.layers.smbserver import SMB_Server 

203 

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

205 return SMB_Server.spawn( 

206 iface=iface or conf.iface, 

207 port=port, 

208 bg=bg, 

209 # Important: pass the DCE/RPC server 

210 DCERPC_SERVER_CLS=cls, 

211 # SMB parameters 

212 **kwargs, 

213 ) 

214 else: 

215 raise ValueError("Unsupported transport :(") 

216 

217 def recv(self, data): 

218 if isinstance(data, bytes): 

219 req = DceRpc5(data) 

220 else: 

221 req = data 

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

223 pad = None 

224 if conf.padding_layer in req: 

225 pad = req[conf.padding_layer].load 

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

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

228 req = self.session.in_pkt(req) 

229 hdr = DceRpc5( 

230 endian=req.endian, 

231 encoding=req.encoding, 

232 float=req.float, 

233 call_id=req.call_id, 

234 ) 

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

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

237 # Log 

238 if self.verb: 

239 print( 

240 conf.color_theme.opening( 

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

242 + ( 

243 " (with %s%s)" 

244 % ( 

245 self.session.ssp.__class__.__name__, 

246 ( 

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

248 if self.session.auth_level is not None 

249 else "" 

250 ), 

251 ) 

252 if self.session.ssp 

253 else "" 

254 ) 

255 ) 

256 ) 

257 if not self.session.rpc_bind_interface: 

258 # The session did not find a matching interface ! 

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

260 if self.verb: 

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

262 else: 

263 auth_value, status = None, 0 

264 if ( 

265 self.session.ssp 

266 and req.auth_verifier 

267 and req.auth_verifier.auth_value 

268 ): 

269 ( 

270 self.session.sspcontext, 

271 auth_value, 

272 status, 

273 ) = self.session.ssp.GSS_Accept_sec_context( 

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

275 ) 

276 self.session.auth_level = RPC_C_AUTHN_LEVEL( 

277 req.auth_verifier.auth_level 

278 ) 

279 self.session.auth_context_id = req.auth_verifier.auth_context_id 

280 if DceRpc5Auth3 in req: 

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

282 if status != 0: 

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

284 if pad is not None: 

285 self.recv(pad) 

286 return 

287 # auth_verifier here contains the SSP nego packets 

288 # (whereas it usually contains the verifiers) 

289 if auth_value is not None: 

290 hdr.auth_verifier = CommonAuthVerifier( 

291 auth_type=req.auth_verifier.auth_type, 

292 auth_level=req.auth_verifier.auth_level, 

293 auth_context_id=req.auth_verifier.auth_context_id, 

294 auth_value=auth_value, 

295 ) 

296 

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

298 self.ndr64 = self.ndr64 and any( 

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

300 for ctx in req.context_elem 

301 ) 

302 

303 # Process bind contexts and answer to them 

304 results = [] 

305 for ctx in req.context_elem: 

306 # Get name 

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

308 if ( 

309 # NDR64 

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

311 or 

312 # NDR 2.0 

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

314 ): 

315 # Acceptance 

316 results.append( 

317 DceRpc5Result( 

318 result=0, 

319 reason=0, 

320 transfer_syntax=DceRpc5TransferSyntax( 

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

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

323 ), 

324 ) 

325 ) 

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

327 # Handle Bind Time Feature 

328 results.append( 

329 DceRpc5Result( 

330 result=3, 

331 reason=3, 

332 transfer_syntax=DceRpc5TransferSyntax( 

333 if_uuid="NULL", 

334 if_version=0, 

335 ), 

336 ) 

337 ) 

338 else: 

339 # Reject 

340 results.append( 

341 DceRpc5Result( 

342 result=2, 

343 reason=2, 

344 transfer_syntax=DceRpc5TransferSyntax( 

345 if_uuid="NULL", 

346 if_version=0, 

347 ), 

348 ) 

349 ) 

350 

351 if self.port is None: 

352 # Piped 

353 port_spec = ( 

354 b"\\\\PIPE\\\\%s\0" 

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

356 ) 

357 else: 

358 # IP 

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

360 if DceRpc5Bind in req: 

361 cls = DceRpc5BindAck 

362 else: 

363 cls = DceRpc5AlterContextResp 

364 self.queue.extend( 

365 self.session.out_pkt( 

366 hdr 

367 / cls( 

368 assoc_group_id=RandShort(), 

369 sec_addr=DceRpc5PortAny( 

370 port_spec=port_spec, 

371 ), 

372 results=results, 

373 ), 

374 ) 

375 ) 

376 if self.verb: 

377 print( 

378 conf.color_theme.success( 

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

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

381 "NDR64" if self.ndr64 else "NDR32" 

382 ) 

383 ) 

384 ) 

385 elif DceRpc5Request in req: 

386 if self.verb: 

387 print( 

388 conf.color_theme.opening( 

389 "<< REQUEST: %s" 

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

391 ) 

392 ) 

393 # Can be any RPC request ! 

394 resp = self.make_reply(req) 

395 if resp: 

396 self.queue.extend( 

397 self.session.out_pkt( 

398 hdr 

399 / DceRpc5Response( 

400 alloc_hint=len(resp), 

401 cont_id=req.cont_id, 

402 ) 

403 / resp, 

404 ) 

405 ) 

406 if self.verb: 

407 print( 

408 conf.color_theme.success( 

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

410 ) 

411 ) 

412 # If there was padding, process the second frag 

413 if pad is not None: 

414 self.recv(pad) 

415 

416 def get_response(self): 

417 try: 

418 return self.queue.popleft() 

419 except IndexError: 

420 return None 

421 

422 # Endpoint mapper 

423 

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

425 def ept_map(self, req): 

426 """ 

427 Answer to ept_map_Request. 

428 """ 

429 if self.transport != DCERPC_Transport.NCACN_IP_TCP: 

430 raise ValueError("Unimplemented") 

431 

432 tower = protocol_tower_t( 

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

434 ) 

435 uuid = tower.floors[0].uuid 

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

437 

438 # Check for results in our portmap 

439 port = None 

440 if (uuid, if_version) in DCE_RPC_INTERFACES: 

441 interface = DCE_RPC_INTERFACES[(uuid, if_version)] 

442 if interface in self.portmap: 

443 port = self.portmap[interface] 

444 

445 if port is not None: 

446 # Found result 

447 resp_tower = twr_p_t( 

448 tower_octet_string=bytes( 

449 protocol_tower_t( 

450 floors=[ 

451 tower.floors[0], # UUID 

452 tower.floors[1], # NDR version 

453 tower.floors[2], # RPC version 

454 prot_and_addr_t( 

455 lhs_length=1, 

456 protocol_identifier="NCACN_IP_TCP", 

457 rhs_length=2, 

458 rhs=port, 

459 ), 

460 prot_and_addr_t( 

461 lhs_length=1, 

462 protocol_identifier="IP", 

463 rhs_length=4, 

464 rhs=self.local_ip or "0.0.0.0", 

465 ), 

466 ] 

467 ) 

468 ) 

469 ) 

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

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

472 else: 

473 # No result found 

474 pass 

475 return resp