unix.py 17 KB

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