Bugzilla – Attachment 2772 Details for
Bug 2511
Drop fine-grained privileges on Illumos/Solaris
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Requests
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
patch-v4
0001-Support-for-fine-grained-Illumos-Solaris-privileges.patch (text/plain), 17.21 KB, created by
Alex Wilson
on 2015-12-14 20:44:14 AEDT
(
hide
)
Description:
patch-v4
Filename:
MIME Type:
Creator:
Alex Wilson
Created:
2015-12-14 20:44:14 AEDT
Size:
17.21 KB
patch
obsolete
>From 171f70c8149fee1aec9451aad057bb41e70fba57 Mon Sep 17 00:00:00 2001 >From: Alex Wilson <alex.wilson@joyent.com> >Date: Tue, 4 Aug 2015 15:50:09 -0700 >Subject: [PATCH] Support for fine-grained Illumos/Solaris privileges > >--- > Makefile.in | 3 +- > configure.ac | 38 +++++++++++++- > mux.c | 2 + > openbsd-compat/port-solaris.c | 114 ++++++++++++++++++++++++++++++++++++++++++ > openbsd-compat/port-solaris.h | 3 ++ > platform.c | 40 +++++++++++++++ > platform.h | 3 ++ > sandbox-solaris.c | 107 +++++++++++++++++++++++++++++++++++++++ > sftp-server.c | 3 ++ > ssh-agent.c | 1 + > uidswap.c | 18 ++++--- > 11 files changed, 323 insertions(+), 9 deletions(-) > create mode 100644 sandbox-solaris.c > >diff --git a/Makefile.in b/Makefile.in >index 15cf69e..f44e4dd 100644 >--- a/Makefile.in >+++ b/Makefile.in >@@ -110,7 +110,8 @@ SSHDOBJS=sshd.o auth-rhosts.o auth-passwd.o auth-rsa.o auth-rh-rsa.o \ > sftp-server.o sftp-common.o \ > roaming_common.o roaming_serv.o \ > sandbox-null.o sandbox-rlimit.o sandbox-systrace.o sandbox-darwin.o \ >- sandbox-seccomp-filter.o sandbox-capsicum.o sandbox-pledge.o >+ sandbox-seccomp-filter.o sandbox-capsicum.o sandbox-pledge.o \ >+ sandbox-solaris.o > > MANPAGES = moduli.5.out scp.1.out ssh-add.1.out ssh-agent.1.out ssh-keygen.1.out ssh-keyscan.1.out ssh.1.out sshd.8.out sftp-server.8.out sftp.1.out ssh-keysign.8.out ssh-pkcs11-helper.8.out sshd_config.5.out ssh_config.5.out > MANPAGES_IN = moduli.5 scp.1 ssh-add.1 ssh-agent.1 ssh-keygen.1 ssh-keyscan.1 ssh.1 sshd.8 sftp-server.8 sftp.1 ssh-keysign.8 ssh-pkcs11-helper.8 sshd_config.5 ssh_config.5 >diff --git a/configure.ac b/configure.ac >index 85e9843..e56a75e 100644 >--- a/configure.ac >+++ b/configure.ac >@@ -469,6 +469,11 @@ AC_CHECK_HEADERS([sys/un.h], [], [], [ > SIA_MSG="no" > SPC_MSG="no" > SP_MSG="no" >+SPP_MSG="no" >+ >+# Support for Solaris/Illumos privileges (this test is used by both >+# the --with-solaris-privs option and --with-sandbox=solaris). >+SOLARIS_PRIVS="no" > > # Check for some target-specific stuff > case "$host" in >@@ -575,6 +580,8 @@ case "$host" in > LIBS="$LIBS /usr/lib/textreadmode.o" > AC_DEFINE([HAVE_CYGWIN], [1], [Define if you are on Cygwin]) > AC_DEFINE([USE_PIPES], [1], [Use PIPES instead of a socketpair()]) >+ AC_DEFINE([NO_UID_RESTORATION_TEST], [1], >+ [Define to disable UID restoration test]) > AC_DEFINE([DISABLE_SHADOW], [1], > [Define if you want to disable shadow passwords]) > AC_DEFINE([NO_X11_UNIX_SOCKETS], [1], >@@ -889,13 +896,18 @@ mips-sony-bsd|mips-sony-newsos4) > else > AC_MSG_RESULT([no]) > fi >+ AC_CHECK_FUNC([setppriv], >+ [ AC_CHECK_HEADERS([priv.h], [ >+ SOLARIS_PRIVS="yes" >+ ]) >+ ]) > AC_ARG_WITH([solaris-contracts], > [ --with-solaris-contracts Enable Solaris process contracts (experimental)], > [ > AC_CHECK_LIB([contract], [ct_tmpl_activate], > [ AC_DEFINE([USE_SOLARIS_PROCESS_CONTRACTS], [1], > [Define if you have Solaris process contracts]) >- SSHDLIBS="$SSHDLIBS -lcontract" >+ LIBS="$LIBS -lcontract" > SPC_MSG="yes" ], ) > ], > ) >@@ -905,10 +917,27 @@ mips-sony-bsd|mips-sony-newsos4) > AC_CHECK_LIB([project], [setproject], > [ AC_DEFINE([USE_SOLARIS_PROJECTS], [1], > [Define if you have Solaris projects]) >- SSHDLIBS="$SSHDLIBS -lproject" >+ LIBS="$LIBS -lproject" > SP_MSG="yes" ], ) > ], > ) >+ AC_ARG_WITH([solaris-privs], >+ [ --with-solaris-privs Enable Solaris/Illumos privileges (experimental)], >+ [ >+ AC_MSG_CHECKING([for Solaris/Illumos privilege support]) >+ if test "x$SOLARIS_PRIVS" = "xyes" ; then >+ AC_MSG_RESULT([found]) >+ AC_DEFINE([NO_UID_RESTORATION_TEST], [1], >+ [Define to disable UID restoration test]) >+ AC_DEFINE([USE_SOLARIS_PRIVS], [1], >+ [Define if you have Solaris privileges]) >+ SPP_MSG="yes" >+ else >+ AC_MSG_RESULT([not found]) >+ AC_MSG_ERROR([*** must have support for Solaris privileges to use --with-solaris-privs]) >+ fi >+ ], >+ ) > TEST_SHELL=$SHELL # let configure find us a capable shell > ;; > *-*-sunos4*) >@@ -3155,6 +3184,10 @@ elif test "x$sandbox_arg" = "xrlimit" || \ > AC_MSG_ERROR([rlimit sandbox requires select to work with rlimit]) > SANDBOX_STYLE="rlimit" > AC_DEFINE([SANDBOX_RLIMIT], [1], [Sandbox using setrlimit(2)]) >+elif test "x$sandbox_arg" = "xsolaris" || \ >+ ( test -z "$sandbox_arg" && test "x$SOLARIS_PRIVS" = "xyes" ) ; then >+ SANDBOX_STYLE="solaris" >+ AC_DEFINE([SANDBOX_SOLARIS], [1], [Sandbox using Solaris/Illumos privileges]) > elif test -z "$sandbox_arg" || test "x$sandbox_arg" = "xno" || \ > test "x$sandbox_arg" = "xnone" || test "x$sandbox_arg" = "xnull" ; then > SANDBOX_STYLE="none" >@@ -4944,6 +4977,7 @@ echo " MD5 password support: $MD5_MSG" > echo " libedit support: $LIBEDIT_MSG" > echo " Solaris process contract support: $SPC_MSG" > echo " Solaris project support: $SP_MSG" >+echo " Solaris privilege support: $SPP_MSG" > echo " IP address in \$DISPLAY hack: $DISPLAY_HACK_MSG" > echo " Translate v4 in v6 hack: $IPV4_IN6_HACK_MSG" > echo " BSD Auth support: $BSD_AUTH_MSG" >diff --git a/mux.c b/mux.c >index a387467..043307d 100644 >--- a/mux.c >+++ b/mux.c >@@ -1853,6 +1853,7 @@ mux_client_request_session(int fd) > > if (pledge("stdio proc tty", NULL) == -1) > fatal("%s pledge(): %s", __func__, strerror(errno)); >+ platform_drop_mux_privs(); > > debug3("%s: session request sent", __func__); > >@@ -2001,6 +2002,7 @@ mux_client_request_stdio_fwd(int fd) > > if (pledge("stdio proc tty", NULL) == -1) > fatal("%s pledge(): %s", __func__, strerror(errno)); >+ platform_drop_mux_privs(); > > debug3("%s: stdio forward request sent", __func__); > >diff --git a/openbsd-compat/port-solaris.c b/openbsd-compat/port-solaris.c >index 25382f1..962cd16 100644 >--- a/openbsd-compat/port-solaris.c >+++ b/openbsd-compat/port-solaris.c >@@ -227,3 +227,117 @@ solaris_set_default_project(struct passwd *pw) > } > } > #endif /* USE_SOLARIS_PROJECTS */ >+ >+#ifdef USE_SOLARIS_PRIVS >+# ifdef HAVE_PRIV_H >+# include <priv.h> >+# endif >+ >+void >+solaris_drop_privs_pinfo_net_fork_exec(void) >+{ >+ priv_set_t *pset = NULL, *npset = NULL; >+ >+ /* >+ * Note: this variant avoids dropping DAC filesystem rights, in case >+ * the process calling it is running as root and should have the >+ * ability to read/write/chown any file on the system. >+ * >+ * We start with the basic set, then *add* the DAC rights to it while >+ * taking away other parts of BASIC we don't need. Then we intersect >+ * this with our existing PERMITTED set. In this way we keep any >+ * DAC rights we had before, while otherwise reducing ourselves to >+ * the minimum set of privileges we need to proceed. >+ * >+ * This also means we drop any other parts of "root" that we don't >+ * need (e.g. the ability to kill any process, create new device nodes >+ * etc etc). >+ */ >+ >+ if ((pset = priv_allocset()) == NULL || >+ (npset = priv_allocset()) == NULL) >+ fatal("priv_allocset: %s", strerror(errno)); >+ >+ priv_basicset(npset); >+ >+ if (priv_addset(npset, PRIV_FILE_CHOWN) != 0 || >+ priv_addset(npset, PRIV_FILE_DAC_READ) != 0 || >+ priv_addset(npset, PRIV_FILE_DAC_SEARCH) != 0 || >+ priv_addset(npset, PRIV_FILE_DAC_WRITE) != 0 || >+ priv_addset(npset, PRIV_FILE_OWNER) != 0) >+ fatal("priv_addset: %s", strerror(errno)); >+ >+ if (priv_delset(npset, PRIV_FILE_LINK_ANY) != 0 || >+ priv_delset(npset, PRIV_NET_ACCESS) != 0 || >+ priv_delset(npset, PRIV_PROC_EXEC) != 0 || >+ priv_delset(npset, PRIV_PROC_FORK) != 0 || >+ priv_delset(npset, PRIV_PROC_INFO) != 0 || >+ priv_delset(npset, PRIV_PROC_SESSION) != 0) >+ fatal("priv_delset: %s", strerror(errno)); >+ >+ if (getppriv(PRIV_PERMITTED, pset) != 0) >+ fatal("getppriv: %s", strerror(errno)); >+ >+ priv_intersect(pset, npset); >+ >+ if (setppriv(PRIV_SET, PRIV_PERMITTED, npset) != 0 || >+ setppriv(PRIV_SET, PRIV_LIMIT, npset) != 0 || >+ setppriv(PRIV_SET, PRIV_INHERITABLE, npset) != 0) >+ fatal("setppriv: %s", strerror(errno)); >+ >+ priv_freeset(pset); >+ priv_freeset(npset); >+} >+ >+void >+solaris_drop_privs_root_pinfo_net(void) >+{ >+ priv_set_t *pset = NULL; >+ >+ if ((pset = priv_allocset()) == NULL) >+ fatal("priv_allocset: %s", strerror(errno)); >+ >+ /* Start with "basic" and drop everything we don't need. */ >+ priv_basicset(pset); >+ >+ if (priv_delset(pset, PRIV_FILE_LINK_ANY) != 0 || >+ priv_delset(pset, PRIV_NET_ACCESS) != 0 || >+ priv_delset(pset, PRIV_PROC_INFO) != 0 || >+ priv_delset(pset, PRIV_PROC_SESSION) != 0) >+ fatal("priv_delset: %s", strerror(errno)); >+ >+ if (setppriv(PRIV_SET, PRIV_PERMITTED, pset) != 0 || >+ setppriv(PRIV_SET, PRIV_LIMIT, pset) != 0 || >+ setppriv(PRIV_SET, PRIV_INHERITABLE, pset) != 0) >+ fatal("setppriv: %s", strerror(errno)); >+ >+ priv_freeset(pset); >+} >+ >+void >+solaris_drop_privs_root_pinfo_net_exec(void) >+{ >+ priv_set_t *pset = NULL; >+ >+ if ((pset = priv_allocset()) == NULL) >+ fatal("priv_allocset: %s", strerror(errno)); >+ >+ /* Start with "basic" and drop everything we don't need. */ >+ priv_basicset(pset); >+ >+ if (priv_delset(pset, PRIV_FILE_LINK_ANY) != 0 || >+ priv_delset(pset, PRIV_NET_ACCESS) != 0 || >+ priv_delset(pset, PRIV_PROC_EXEC) != 0 || >+ priv_delset(pset, PRIV_PROC_INFO) != 0 || >+ priv_delset(pset, PRIV_PROC_SESSION) != 0) >+ fatal("priv_delset: %s", strerror(errno)); >+ >+ if (setppriv(PRIV_SET, PRIV_PERMITTED, pset) != 0 || >+ setppriv(PRIV_SET, PRIV_LIMIT, pset) != 0 || >+ setppriv(PRIV_SET, PRIV_INHERITABLE, pset) != 0) >+ fatal("setppriv: %s", strerror(errno)); >+ >+ priv_freeset(pset); >+} >+ >+#endif >diff --git a/openbsd-compat/port-solaris.h b/openbsd-compat/port-solaris.h >index cd442e7..b077e18 100644 >--- a/openbsd-compat/port-solaris.h >+++ b/openbsd-compat/port-solaris.h >@@ -26,5 +26,8 @@ void solaris_contract_pre_fork(void); > void solaris_contract_post_fork_child(void); > void solaris_contract_post_fork_parent(pid_t pid); > void solaris_set_default_project(struct passwd *); >+void solaris_drop_privs_pinfo_net_fork_exec(void); >+void solaris_drop_privs_root_pinfo_net(void); >+void solaris_drop_privs_root_pinfo_net_exec(void); > > #endif >diff --git a/platform.c b/platform.c >index ee313da..633a065 100644 >--- a/platform.c >+++ b/platform.c >@@ -213,3 +213,43 @@ platform_sys_dir_uid(uid_t uid) > #endif > return 0; > } >+ >+/* >+ * Drop any fine-grained privileges that are not needed for post-startup >+ * operation of ssh-agent >+ * >+ * Should be as close as possible to pledge("stdio cpath unix exec proc", ...) >+ */ >+void >+platform_drop_agent_privs(void) >+{ >+#ifdef USE_SOLARIS_PRIVS >+ solaris_drop_privs_root_pinfo_net(); >+#endif >+} >+ >+/* >+ * Drop any fine-grained privileges that are not needed for post-startup >+ * operation of sftp-server >+ */ >+void >+platform_drop_sftp_server_privs(void) >+{ >+#ifdef USE_SOLARIS_PRIVS >+ solaris_drop_privs_pinfo_net_fork_exec(); >+#endif >+} >+ >+/* >+ * Drop any fine-grained privileges that are not needed for the post-startup >+ * operation of the SSH client mux >+ * >+ * Should be as close as possible to pledge("stdio proc tty", ...) >+ */ >+void >+platform_drop_mux_privs(void) >+{ >+#ifdef USE_SOLARIS_PRIVS >+ solaris_drop_privs_root_pinfo_net_exec(); >+#endif >+} >diff --git a/platform.h b/platform.h >index 1c7a45d..5a81e8f 100644 >--- a/platform.h >+++ b/platform.h >@@ -31,3 +31,6 @@ void platform_setusercontext_post_groups(struct passwd *); > char *platform_get_krb5_client(const char *); > char *platform_krb5_get_principal_name(const char *); > int platform_sys_dir_uid(uid_t); >+void platform_drop_agent_privs(void); >+void platform_drop_sftp_server_privs(void); >+void platform_drop_mux_privs(void); >diff --git a/sandbox-solaris.c b/sandbox-solaris.c >new file mode 100644 >index 0000000..98714e1 >--- /dev/null >+++ b/sandbox-solaris.c >@@ -0,0 +1,107 @@ >+/* >+ * Copyright (c) 2015 Joyent, Inc >+ * Author: Alex Wilson <alex.wilson@joyent.com> >+ * >+ * Permission to use, copy, modify, and distribute this software for any >+ * purpose with or without fee is hereby granted, provided that the above >+ * copyright notice and this permission notice appear in all copies. >+ * >+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES >+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF >+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR >+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES >+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN >+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF >+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. >+ */ >+ >+#include "includes.h" >+ >+#ifdef SANDBOX_SOLARIS >+#ifndef USE_SOLARIS_PRIVS >+# error "--with-solaris-privs must be used with the Solaris sandbox" >+#endif >+ >+#include <sys/types.h> >+ >+#include <errno.h> >+#include <stdarg.h> >+#include <stdio.h> >+#include <stdlib.h> >+#include <string.h> >+#include <unistd.h> >+#ifdef HAVE_PRIV_H >+# include <priv.h> >+#endif >+ >+#include "log.h" >+#include "ssh-sandbox.h" >+#include "xmalloc.h" >+ >+struct ssh_sandbox { >+ priv_set_t *pset; >+}; >+ >+struct ssh_sandbox * >+ssh_sandbox_init(struct monitor *monitor) >+{ >+ struct ssh_sandbox *box = NULL; >+ >+ box = xcalloc(1, sizeof(*box)); >+ box->pset = priv_allocset(); >+ >+ if (box->pset == NULL) { >+ free(box); >+ return NULL; >+ } >+ >+ /* Start with "basic" and drop everything we don't need. */ >+ priv_basicset(box->pset); >+ >+ /* Drop everything except the ability to use already-opened files */ >+ if (priv_delset(box->pset, PRIV_FILE_LINK_ANY) != 0 || >+ priv_delset(box->pset, PRIV_NET_ACCESS) != 0 || >+ priv_delset(box->pset, PRIV_PROC_EXEC) != 0 || >+ priv_delset(box->pset, PRIV_PROC_FORK) != 0 || >+ priv_delset(box->pset, PRIV_PROC_INFO) != 0 || >+ priv_delset(box->pset, PRIV_PROC_SESSION) != 0) { >+ free(box); >+ return NULL; >+ } >+ >+ /* These may not be available on older Solaris-es */ >+# if defined(PRIV_FILE_READ) && defined(PRIV_FILE_WRITE) >+ if (priv_delset(box->pset, PRIV_FILE_READ) != 0 || >+ priv_delset(box->pset, PRIV_FILE_WRITE) != 0) { >+ free(box); >+ return NULL; >+ } >+# endif >+ >+ return box; >+} >+ >+void >+ssh_sandbox_child(struct ssh_sandbox *box) >+{ >+ if (setppriv(PRIV_SET, PRIV_PERMITTED, box->pset) != 0 || >+ setppriv(PRIV_SET, PRIV_LIMIT, box->pset) != 0 || >+ setppriv(PRIV_SET, PRIV_INHERITABLE, box->pset) != 0) >+ fatal("setppriv: %s", strerror(errno)); >+} >+ >+void >+ssh_sandbox_parent_finish(struct ssh_sandbox *box) >+{ >+ priv_freeset(box->pset); >+ box->pset = NULL; >+ free(box); >+} >+ >+void >+ssh_sandbox_parent_preauth(struct ssh_sandbox *box, pid_t child_pid) >+{ >+ /* Nothing to do here */ >+} >+ >+#endif /* SANDBOX_SOLARIS */ >diff --git a/sftp-server.c b/sftp-server.c >index 62e76a5..c132817 100644 >--- a/sftp-server.c >+++ b/sftp-server.c >@@ -1598,6 +1598,9 @@ sftp_server_main(int argc, char **argv, struct passwd *user_pw) > fatal("unable to make the process undumpable"); > #endif /* defined(HAVE_PRCTL) && defined(PR_SET_DUMPABLE) */ > >+ /* Drop any fine-grained privileges we don't need */ >+ platform_drop_sftp_server_privs(); >+ > if ((cp = getenv("SSH_CONNECTION")) != NULL) { > client_addr = xstrdup(cp); > if ((cp = strchr(client_addr, ' ')) == NULL) { >diff --git a/ssh-agent.c b/ssh-agent.c >index d384cfb..2207fb7 100644 >--- a/ssh-agent.c >+++ b/ssh-agent.c >@@ -1416,6 +1416,7 @@ skip: > > if (pledge("stdio cpath unix exec proc", NULL) != 0) > fatal("%s: pledge: %s", __progname, strerror(errno)); >+ platform_drop_agent_privs(); > > while (1) { > prepare_select(&readsetp, &writesetp, &max_fd, &nalloc, &tvp); >diff --git a/uidswap.c b/uidswap.c >index 0702e1d..8bf6b24 100644 >--- a/uidswap.c >+++ b/uidswap.c >@@ -134,7 +134,7 @@ temporarily_use_uid(struct passwd *pw) > void > permanently_drop_suid(uid_t uid) > { >-#ifndef HAVE_CYGWIN >+#ifndef NO_UID_RESTORATION_TEST > uid_t old_uid = getuid(); > #endif > >@@ -142,8 +142,14 @@ permanently_drop_suid(uid_t uid) > if (setresuid(uid, uid, uid) < 0) > fatal("setresuid %u: %.100s", (u_int)uid, strerror(errno)); > >-#ifndef HAVE_CYGWIN >- /* Try restoration of UID if changed (test clearing of saved uid) */ >+#ifndef NO_UID_RESTORATION_TEST >+ /* >+ * Try restoration of UID if changed (test clearing of saved uid). >+ * >+ * Note that we don't do this on Cygwin, or on Solaris-based platforms >+ * where fine-grained privileges are available (the user might be >+ * deliberately allowed the right to setuid back to root). >+ */ > if (old_uid != uid && > (setuid(old_uid) != -1 || seteuid(old_uid) != -1)) > fatal("%s: was able to restore old [e]uid", __func__); >@@ -199,7 +205,7 @@ restore_uid(void) > void > permanently_set_uid(struct passwd *pw) > { >-#ifndef HAVE_CYGWIN >+#ifndef NO_UID_RESTORATION_TEST > uid_t old_uid = getuid(); > gid_t old_gid = getgid(); > #endif >@@ -227,7 +233,7 @@ permanently_set_uid(struct passwd *pw) > if (setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid) < 0) > fatal("setresuid %u: %.100s", (u_int)pw->pw_uid, strerror(errno)); > >-#ifndef HAVE_CYGWIN >+#ifndef NO_UID_RESTORATION_TEST > /* Try restoration of GID if changed (test clearing of saved gid) */ > if (old_gid != pw->pw_gid && pw->pw_uid != 0 && > (setgid(old_gid) != -1 || setegid(old_gid) != -1)) >@@ -241,7 +247,7 @@ permanently_set_uid(struct passwd *pw) > (u_int)pw->pw_gid); > } > >-#ifndef HAVE_CYGWIN >+#ifndef NO_UID_RESTORATION_TEST > /* Try restoration of UID if changed (test clearing of saved uid) */ > if (old_uid != pw->pw_uid && > (setuid(old_uid) != -1 || seteuid(old_uid) != -1)) >-- >2.5.4 (Apple Git-61) >
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Flags:
dtucker
:
ok+
Actions:
View
|
Diff
Attachments on
bug 2511
:
2761
|
2770
|
2771
|
2772
|
2774