1# Copyright (c) Twisted Matrix Laboratories. 2# See LICENSE for details. 3 4""" 5A UNIX SSH server. 6""" 7 8import fcntl 9import grp 10import os 11import pty 12import pwd 13import socket 14import struct 15import time 16import tty 17 18from zope.interface import implementer 19 20from twisted.conch import ttymodes 21from twisted.conch.avatar import ConchUser 22from twisted.conch.error import ConchError 23from twisted.conch.interfaces import ISession, ISFTPFile, ISFTPServer 24from twisted.conch.ls import lsLine 25from twisted.conch.ssh import filetransfer, forwarding, session 26from twisted.conch.ssh.filetransfer import ( 27 FXF_APPEND, 28 FXF_CREAT, 29 FXF_EXCL, 30 FXF_READ, 31 FXF_TRUNC, 32 FXF_WRITE, 33) 34from twisted.cred import portal 35from twisted.internet.error import ProcessExitedAlready 36from twisted.logger import Logger 37from twisted.python import components 38from twisted.python.compat import nativeString 39 40try: 41 import utmp # type: ignore[import] 42except ImportError: 43 utmp = None 44 45 46@implementer(portal.IRealm) 47class UnixSSHRealm: 48 def requestAvatar(self, username, mind, *interfaces): 49 user = UnixConchUser(username) 50 return interfaces[0], user, user.logout 51 52 53class UnixConchUser(ConchUser): 54 def __init__(self, username): 55 ConchUser.__init__(self) 56 self.username = username 57 self.pwdData = pwd.getpwnam(self.username) 58 l = [self.pwdData[3]] 59 for groupname, password, gid, userlist in grp.getgrall(): 60 if username in userlist: 61 l.append(gid) 62 self.otherGroups = l 63 self.listeners = {} # Dict mapping (interface, port) -> listener 64 self.channelLookup.update( 65 { 66 b"session": session.SSHSession, 67 b"direct-tcpip": forwarding.openConnectForwardingClient, 68 } 69 ) 70 71 self.subsystemLookup.update({b"sftp": filetransfer.FileTransferServer}) 72 73 def getUserGroupId(self): 74 return self.pwdData[2:4] 75 76 def getOtherGroups(self): 77 return self.otherGroups 78 79 def getHomeDir(self): 80 return self.pwdData[5] 81 82 def getShell(self): 83 return self.pwdData[6] 84 85 def global_tcpip_forward(self, data): 86 hostToBind, portToBind = forwarding.unpackGlobal_tcpip_forward(data) 87 from twisted.internet import reactor 88 89 try: 90 listener = self._runAsUser( 91 reactor.listenTCP, 92 portToBind, 93 forwarding.SSHListenForwardingFactory( 94 self.conn, 95 (hostToBind, portToBind), 96 forwarding.SSHListenServerForwardingChannel, 97 ), 98 interface=hostToBind, 99 ) 100 except BaseException: 101 return 0 102 else: 103 self.listeners[(hostToBind, portToBind)] = listener 104 if portToBind == 0: 105 portToBind = listener.getHost()[2] # The port 106 return 1, struct.pack(">L", portToBind) 107 else: 108 return 1 109 110 def global_cancel_tcpip_forward(self, data): 111 hostToBind, portToBind = forwarding.unpackGlobal_tcpip_forward(data) 112 listener = self.listeners.get((hostToBind, portToBind), None) 113 if not listener: 114 return 0 115 del self.listeners[(hostToBind, portToBind)] 116 self._runAsUser(listener.stopListening) 117 return 1 118 119 def logout(self): 120 # Remove all listeners. 121 for listener in self.listeners.values(): 122 self._runAsUser(listener.stopListening) 123 self._log.info( 124 "avatar {username} logging out ({nlisteners})", 125 username=self.username, 126 nlisteners=len(self.listeners), 127 ) 128 129 def _runAsUser(self, f, *args, **kw): 130 euid = os.geteuid() 131 egid = os.getegid() 132 groups = os.getgroups() 133 uid, gid = self.getUserGroupId() 134 os.setegid(0) 135 os.seteuid(0) 136 os.setgroups(self.getOtherGroups()) 137 os.setegid(gid) 138 os.seteuid(uid) 139 try: 140 f = iter(f) 141 except TypeError: 142 f = [(f, args, kw)] 143 try: 144 for i in f: 145 func = i[0] 146 args = len(i) > 1 and i[1] or () 147 kw = len(i) > 2 and i[2] or {} 148 r = func(*args, **kw) 149 finally: 150 os.setegid(0) 151 os.seteuid(0) 152 os.setgroups(groups) 153 os.setegid(egid) 154 os.seteuid(euid) 155 return r 156 157 158@implementer(ISession) 159class SSHSessionForUnixConchUser: 160 _log = Logger() 161 162 def __init__(self, avatar, reactor=None): 163 """ 164 Construct an C{SSHSessionForUnixConchUser}. 165 166 @param avatar: The L{UnixConchUser} for whom this is an SSH session. 167 @param reactor: An L{IReactorProcess} used to handle shell and exec 168 requests. Uses the default reactor if None. 169 """ 170 if reactor is None: 171 from twisted.internet import reactor 172 self._reactor = reactor 173 self.avatar = avatar 174 self.environ = {"PATH": "/bin:/usr/bin:/usr/local/bin"} 175 self.pty = None 176 self.ptyTuple = 0 177 178 def addUTMPEntry(self, loggedIn=1): 179 if not utmp: 180 return 181 ipAddress = self.avatar.conn.transport.transport.getPeer().host 182 (packedIp,) = struct.unpack("L", socket.inet_aton(ipAddress)) 183 ttyName = self.ptyTuple[2][5:] 184 t = time.time() 185 t1 = int(t) 186 t2 = int((t - t1) * 1e6) 187 entry = utmp.UtmpEntry() 188 entry.ut_type = loggedIn and utmp.USER_PROCESS or utmp.DEAD_PROCESS 189 entry.ut_pid = self.pty.pid 190 entry.ut_line = ttyName 191 entry.ut_id = ttyName[-4:] 192 entry.ut_tv = (t1, t2) 193 if loggedIn: 194 entry.ut_user = self.avatar.username 195 entry.ut_host = socket.gethostbyaddr(ipAddress)[0] 196 entry.ut_addr_v6 = (packedIp, 0, 0, 0) 197 a = utmp.UtmpRecord(utmp.UTMP_FILE) 198 a.pututline(entry) 199 a.endutent() 200 b = utmp.UtmpRecord(utmp.WTMP_FILE) 201 b.pututline(entry) 202 b.endutent() 203 204 def getPty(self, term, windowSize, modes): 205 self.environ["TERM"] = term 206 self.winSize = windowSize 207 self.modes = modes 208 master, slave = pty.openpty() 209 ttyname = os.ttyname(slave) 210 self.environ["SSH_TTY"] = ttyname 211 self.ptyTuple = (master, slave, ttyname) 212 213 def openShell(self, proto): 214 if not self.ptyTuple: # We didn't get a pty-req. 215 self._log.error("tried to get shell without pty, failing") 216 raise ConchError("no pty") 217 uid, gid = self.avatar.getUserGroupId() 218 homeDir = self.avatar.getHomeDir() 219 shell = self.avatar.getShell() 220 self.environ["USER"] = self.avatar.username 221 self.environ["HOME"] = homeDir 222 self.environ["SHELL"] = shell 223 shellExec = os.path.basename(shell) 224 peer = self.avatar.conn.transport.transport.getPeer() 225 host = self.avatar.conn.transport.transport.getHost() 226 self.environ["SSH_CLIENT"] = f"{peer.host} {peer.port} {host.port}" 227 self.getPtyOwnership() 228 self.pty = self._reactor.spawnProcess( 229 proto, 230 shell, 231 [f"-{shellExec}"], 232 self.environ, 233 homeDir, 234 uid, 235 gid, 236 usePTY=self.ptyTuple, 237 ) 238 self.addUTMPEntry() 239 fcntl.ioctl(self.pty.fileno(), tty.TIOCSWINSZ, struct.pack("4H", *self.winSize)) 240 if self.modes: 241 self.setModes() 242 self.oldWrite = proto.transport.write 243 proto.transport.write = self._writeHack 244 self.avatar.conn.transport.transport.setTcpNoDelay(1) 245 246 def execCommand(self, proto, cmd): 247 uid, gid = self.avatar.getUserGroupId() 248 homeDir = self.avatar.getHomeDir() 249 shell = self.avatar.getShell() or "/bin/sh" 250 self.environ["HOME"] = homeDir 251 command = (shell, "-c", cmd) 252 peer = self.avatar.conn.transport.transport.getPeer() 253 host = self.avatar.conn.transport.transport.getHost() 254 self.environ["SSH_CLIENT"] = f"{peer.host} {peer.port} {host.port}" 255 if self.ptyTuple: 256 self.getPtyOwnership() 257 self.pty = self._reactor.spawnProcess( 258 proto, 259 shell, 260 command, 261 self.environ, 262 homeDir, 263 uid, 264 gid, 265 usePTY=self.ptyTuple or 0, 266 ) 267 if self.ptyTuple: 268 self.addUTMPEntry() 269 if self.modes: 270 self.setModes() 271 self.avatar.conn.transport.transport.setTcpNoDelay(1) 272 273 def getPtyOwnership(self): 274 ttyGid = os.stat(self.ptyTuple[2])[5] 275 uid, gid = self.avatar.getUserGroupId() 276 euid, egid = os.geteuid(), os.getegid() 277 os.setegid(0) 278 os.seteuid(0) 279 try: 280 os.chown(self.ptyTuple[2], uid, ttyGid) 281 finally: 282 os.setegid(egid) 283 os.seteuid(euid) 284 285 def setModes(self): 286 pty = self.pty 287 attr = tty.tcgetattr(pty.fileno()) 288 for mode, modeValue in self.modes: 289 if mode not in ttymodes.TTYMODES: 290 continue 291 ttyMode = ttymodes.TTYMODES[mode] 292 if len(ttyMode) == 2: # Flag. 293 flag, ttyAttr = ttyMode 294 if not hasattr(tty, ttyAttr): 295 continue 296 ttyval = getattr(tty, ttyAttr) 297 if modeValue: 298 attr[flag] = attr[flag] | ttyval 299 else: 300 attr[flag] = attr[flag] & ~ttyval 301 elif ttyMode == "OSPEED": 302 attr[tty.OSPEED] = getattr(tty, f"B{modeValue}") 303 elif ttyMode == "ISPEED": 304 attr[tty.ISPEED] = getattr(tty, f"B{modeValue}") 305 else: 306 if not hasattr(tty, ttyMode): 307 continue 308 ttyval = getattr(tty, ttyMode) 309 attr[tty.CC][ttyval] = bytes((modeValue,)) 310 tty.tcsetattr(pty.fileno(), tty.TCSANOW, attr) 311 312 def eofReceived(self): 313 if self.pty: 314 self.pty.closeStdin() 315 316 def closed(self): 317 if self.ptyTuple and os.path.exists(self.ptyTuple[2]): 318 ttyGID = os.stat(self.ptyTuple[2])[5] 319 os.chown(self.ptyTuple[2], 0, ttyGID) 320 if self.pty: 321 try: 322 self.pty.signalProcess("HUP") 323 except (OSError, ProcessExitedAlready): 324 pass 325 self.pty.loseConnection() 326 self.addUTMPEntry(0) 327 self._log.info("shell closed") 328 329 def windowChanged(self, winSize): 330 self.winSize = winSize 331 fcntl.ioctl(self.pty.fileno(), tty.TIOCSWINSZ, struct.pack("4H", *self.winSize)) 332 333 def _writeHack(self, data): 334 """ 335 Hack to send ignore messages when we aren't echoing. 336 """ 337 if self.pty is not None: 338 attr = tty.tcgetattr(self.pty.fileno())[3] 339 if not attr & tty.ECHO and attr & tty.ICANON: # No echo. 340 self.avatar.conn.transport.sendIgnore("\x00" * (8 + len(data))) 341 self.oldWrite(data) 342 343 344@implementer(ISFTPServer) 345class SFTPServerForUnixConchUser: 346 def __init__(self, avatar): 347 self.avatar = avatar 348 349 def _setAttrs(self, path, attrs): 350 """ 351 NOTE: this function assumes it runs as the logged-in user: 352 i.e. under _runAsUser() 353 """ 354 if "uid" in attrs and "gid" in attrs: 355 os.chown(path, attrs["uid"], attrs["gid"]) 356 if "permissions" in attrs: 357 os.chmod(path, attrs["permissions"]) 358 if "atime" in attrs and "mtime" in attrs: 359 os.utime(path, (attrs["atime"], attrs["mtime"])) 360 361 def _getAttrs(self, s): 362 return { 363 "size": s.st_size, 364 "uid": s.st_uid, 365 "gid": s.st_gid, 366 "permissions": s.st_mode, 367 "atime": int(s.st_atime), 368 "mtime": int(s.st_mtime), 369 } 370 371 def _absPath(self, path): 372 home = self.avatar.getHomeDir() 373 return os.path.join(nativeString(home.path), nativeString(path)) 374 375 def gotVersion(self, otherVersion, extData): 376 return {} 377 378 def openFile(self, filename, flags, attrs): 379 return UnixSFTPFile(self, self._absPath(filename), flags, attrs) 380 381 def removeFile(self, filename): 382 filename = self._absPath(filename) 383 return self.avatar._runAsUser(os.remove, filename) 384 385 def renameFile(self, oldpath, newpath): 386 oldpath = self._absPath(oldpath) 387 newpath = self._absPath(newpath) 388 return self.avatar._runAsUser(os.rename, oldpath, newpath) 389 390 def makeDirectory(self, path, attrs): 391 path = self._absPath(path) 392 return self.avatar._runAsUser( 393 [(os.mkdir, (path,)), (self._setAttrs, (path, attrs))] 394 ) 395 396 def removeDirectory(self, path): 397 path = self._absPath(path) 398 self.avatar._runAsUser(os.rmdir, path) 399 400 def openDirectory(self, path): 401 return UnixSFTPDirectory(self, self._absPath(path)) 402 403 def getAttrs(self, path, followLinks): 404 path = self._absPath(path) 405 if followLinks: 406 s = self.avatar._runAsUser(os.stat, path) 407 else: 408 s = self.avatar._runAsUser(os.lstat, path) 409 return self._getAttrs(s) 410 411 def setAttrs(self, path, attrs): 412 path = self._absPath(path) 413 self.avatar._runAsUser(self._setAttrs, path, attrs) 414 415 def readLink(self, path): 416 path = self._absPath(path) 417 return self.avatar._runAsUser(os.readlink, path) 418 419 def makeLink(self, linkPath, targetPath): 420 linkPath = self._absPath(linkPath) 421 targetPath = self._absPath(targetPath) 422 return self.avatar._runAsUser(os.symlink, targetPath, linkPath) 423 424 def realPath(self, path): 425 return os.path.realpath(self._absPath(path)) 426 427 def extendedRequest(self, extName, extData): 428 raise NotImplementedError 429 430 431@implementer(ISFTPFile) 432class UnixSFTPFile: 433 def __init__(self, server, filename, flags, attrs): 434 self.server = server 435 openFlags = 0 436 if flags & FXF_READ == FXF_READ and flags & FXF_WRITE == 0: 437 openFlags = os.O_RDONLY 438 if flags & FXF_WRITE == FXF_WRITE and flags & FXF_READ == 0: 439 openFlags = os.O_WRONLY 440 if flags & FXF_WRITE == FXF_WRITE and flags & FXF_READ == FXF_READ: 441 openFlags = os.O_RDWR 442 if flags & FXF_APPEND == FXF_APPEND: 443 openFlags |= os.O_APPEND 444 if flags & FXF_CREAT == FXF_CREAT: 445 openFlags |= os.O_CREAT 446 if flags & FXF_TRUNC == FXF_TRUNC: 447 openFlags |= os.O_TRUNC 448 if flags & FXF_EXCL == FXF_EXCL: 449 openFlags |= os.O_EXCL 450 if "permissions" in attrs: 451 mode = attrs["permissions"] 452 del attrs["permissions"] 453 else: 454 mode = 0o777 455 fd = server.avatar._runAsUser(os.open, filename, openFlags, mode) 456 if attrs: 457 server.avatar._runAsUser(server._setAttrs, filename, attrs) 458 self.fd = fd 459 460 def close(self): 461 return self.server.avatar._runAsUser(os.close, self.fd) 462 463 def readChunk(self, offset, length): 464 return self.server.avatar._runAsUser( 465 [(os.lseek, (self.fd, offset, 0)), (os.read, (self.fd, length))] 466 ) 467 468 def writeChunk(self, offset, data): 469 return self.server.avatar._runAsUser( 470 [(os.lseek, (self.fd, offset, 0)), (os.write, (self.fd, data))] 471 ) 472 473 def getAttrs(self): 474 s = self.server.avatar._runAsUser(os.fstat, self.fd) 475 return self.server._getAttrs(s) 476 477 def setAttrs(self, attrs): 478 raise NotImplementedError 479 480 481class UnixSFTPDirectory: 482 def __init__(self, server, directory): 483 self.server = server 484 self.files = server.avatar._runAsUser(os.listdir, directory) 485 self.dir = directory 486 487 def __iter__(self): 488 return self 489 490 def __next__(self): 491 try: 492 f = self.files.pop(0) 493 except IndexError: 494 raise StopIteration 495 else: 496 s = self.server.avatar._runAsUser(os.lstat, os.path.join(self.dir, f)) 497 longname = lsLine(f, s) 498 attrs = self.server._getAttrs(s) 499 return (f, longname, attrs) 500 501 next = __next__ 502 503 def close(self): 504 self.files = [] 505 506 507components.registerAdapter( 508 SFTPServerForUnixConchUser, UnixConchUser, filetransfer.ISFTPServer 509) 510components.registerAdapter(SSHSessionForUnixConchUser, UnixConchUser, session.ISession) 511