star/net/sshjar.go
2025-03-25 10:00:26 +08:00

371 lines
11 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package net
import (
"b612.me/starcrypto"
"b612.me/starlog"
"b612.me/starnet"
"crypto/elliptic"
"encoding/csv"
"fmt"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/terminal"
"math/rand"
"net"
"os"
"strings"
"time"
)
var (
listenAddr string
keyFile string
KeyPasswd string
outpath string
curlUrl string
serverVersion string
curlArg []string
passwds []string
allowAny bool
logPath string
)
func init() {
cmdSSHJar.Flags().StringVarP(&listenAddr, "listen", "l", "0.0.0.0:22", "监听地址")
cmdSSHJar.Flags().StringVarP(&keyFile, "key", "k", "", "私钥文件")
cmdSSHJar.Flags().StringVarP(&KeyPasswd, "passwd", "p", "", "私钥密码")
cmdSSHJar.Flags().StringVarP(&outpath, "output", "o", "", "输出文件")
cmdSSHJar.Flags().StringVarP(&serverVersion, "version", "v", "SSH-2.0-OpenSSH_8.0", "SSH版本")
cmdSSHJar.Flags().StringVarP(&curlUrl, "curl", "c", "", "Curl URL")
cmdSSHJar.Flags().StringSliceVarP(&passwds, "allow-passwds", "P", nil, "密码列表,格式:[用户名]:[密码]")
cmdSSHJar.Flags().BoolVarP(&allowAny, "allow-any", "A", false, "允许任意密码登录")
cmdSSHJar.Flags().StringVarP(&logPath, "log", "L", "", "日志文件")
}
var cmdSSHJar = &cobra.Command{
Use: "sshjar",
Short: "SSH蜜罐",
Long: "SSH蜜罐",
Run: func(cmd *cobra.Command, args []string) {
var mypwds [][]string
for _, v := range passwds {
args := strings.SplitN(v, ":", 2)
if len(args) == 2 {
mypwds = append(mypwds, args)
}
}
runSSHHoneyJar(SSHJar{
listenAddr: listenAddr,
keyFile: keyFile,
keyPasswd: KeyPasswd,
outpath: outpath,
logpath: logPath,
version: serverVersion,
passwds: mypwds,
allowAny: allowAny,
})
},
}
type SSHJar struct {
listenAddr string
keyFile string
keyPasswd string
outpath string
logpath string
version string
passwds [][]string
allowAny bool
}
func runSSHHoneyJar(jar SSHJar) {
if jar.logpath != "" {
starlog.SetLogFile(jar.logpath, starlog.Std, true)
}
var f *os.File
var err error
if outpath != "" {
f, err = os.OpenFile(outpath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
starlog.Errorf("Failed to open file %s (%s)", outpath, err)
return
}
}
conn := csv.NewWriter(f)
defer f.Close()
defer conn.Flush()
config := &ssh.ServerConfig{
ServerVersion: jar.version,
// 密码验证回调函数
PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
starlog.Infof("Login attempt from %s with %s by %s\n", c.RemoteAddr(), c.User(), string(pass))
data := []string{time.Now().Format("2006-01-02 15:04:05"), c.RemoteAddr().String(), c.User(), string(pass)}
if f != nil {
conn.Write(data)
conn.Flush()
}
if curlUrl != "" {
go func() {
data := map[string]string{
"ip": c.RemoteAddr().String(),
"user": c.User(),
"passwd": string(pass),
}
if curlArg != nil && len(curlArg) > 0 {
for _, v := range curlArg {
args := strings.SplitN(v, ":", 2)
if len(args) == 2 {
data[args[0]] = args[1]
}
}
starnet.NewSimpleRequest(curlUrl, "POST").SetBodyDataBytes([]byte(starnet.BuildQuery(data))).Do()
}
}()
}
perm := &ssh.Permissions{
Extensions: map[string]string{
"user": c.User(),
"passwd": string(pass),
},
}
if jar.allowAny {
return perm, nil
}
for _, v := range jar.passwds {
if c.User() == v[0] && string(pass) == v[1] {
return perm, nil
}
}
return nil, fmt.Errorf("password rejected for %q", c.User())
},
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
if jar.allowAny {
return &ssh.Permissions{
Extensions: map[string]string{
"user": conn.User(),
},
}, nil
}
return nil, fmt.Errorf("public key rejected for %q", conn.User())
},
}
if keyFile == "" {
secKey, _, err := starcrypto.GenerateEcdsaKey(elliptic.P256())
if err != nil {
starlog.Errorf("Failed to generate ECDSA key (%s)", err)
return
}
key, err := ssh.NewSignerFromKey(secKey)
if err != nil {
starlog.Errorf("Failed to generate signer from key (%s)", err)
return
}
config.AddHostKey(key)
} else {
keyByte, err := os.ReadFile(keyFile)
if err != nil {
starlog.Errorf("Failed to read private key from %s (%s)", keyFile, err)
return
}
var key ssh.Signer
if KeyPasswd != "" {
key, err = ssh.ParsePrivateKeyWithPassphrase(keyByte, []byte(KeyPasswd))
} else {
key, err = ssh.ParsePrivateKey(keyByte)
}
if err != nil {
starlog.Errorf("Failed to load private key from %s (%s)", keyFile, err)
return
}
config.AddHostKey(key)
}
listener, err := net.Listen("tcp", listenAddr)
if err != nil {
starlog.Errorf("Failed to listen on %s (%s)", listenAddr, err)
return
}
starlog.Noticeln("SSH HoneyJar is listening on", listenAddr)
for {
conn, err := listener.Accept()
if err != nil {
continue
}
starlog.Infof("New connection from %s\n", conn.RemoteAddr())
go func(conn net.Conn) {
sConn, chans, reqs, err := ssh.NewServerConn(conn, config)
if err != nil {
starlog.Errorf("SSH handshake failed: %v\n", err)
return
}
defer sConn.Close()
defer starlog.Noticef("Connection from %s closed\n", sConn.RemoteAddr())
go ssh.DiscardRequests(reqs)
for newChannel := range chans {
if newChannel.ChannelType() != "session" {
newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")
continue
}
channel, requests, err := newChannel.Accept()
if err != nil {
starlog.Errorf("Failed to accept channel: %v", err)
continue
}
go handleSession(channel, requests, sConn)
}
}(conn)
}
}
func handleSession(channel ssh.Channel, requests <-chan *ssh.Request, conn *ssh.ServerConn) {
defer channel.Close()
term := terminal.NewTerminal(channel, "$ ") // 设置 shell 提示符
term.AutoCompleteCallback = nil // 禁用自动补全
for req := range requests {
switch req.Type {
case "pty-req":
// 接受伪终端请求(攻击者希望获得交互式体验)
req.Reply(true, nil)
term.SetSize( // 简单设置终端尺寸
24, // 行
80, // 列
)
case "shell":
req.Reply(true, nil)
go func() {
for {
line, err := term.ReadLine()
if err != nil {
break
}
starlog.Infof("[%s %s] Command: %s\n", conn.RemoteAddr(), conn.Permissions.Extensions["user"], line)
time.Sleep(time.Millisecond * 200)
term.Write([]byte(FakeCommand(line)))
}
}()
case "exec":
// 处理非交互式命令(如 ssh user@host 'ls -l'
var payload struct{ Command string }
ssh.Unmarshal(req.Payload, &payload)
req.Reply(true, nil)
// 记录并返回假输出
starlog.Infof("[%s %s] Exec: %s\n", conn.RemoteAddr(), conn.Permissions.Extensions["user"], payload.Command)
term.Write([]byte(FakeCommand(payload.Command)))
channel.Close()
default:
req.Reply(false, nil)
}
}
}
func FakeCommand(cmd string) string {
// 按命令类型分级模拟
switch {
//---------------- 系统信息探测类 ----------------
case strings.Contains(cmd, "uname -a"):
return "Linux core 6.1.0-21-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.90-1 (2024-05-03) x86_64 GNU/Linux\n\n"
case strings.Contains(cmd, "cat /etc/os-release"):
return `PRETTY_NAME="Debian GNU/Linux 11 (bullseye)"
NAME="Debian GNU/Linux"
VERSION_ID="11"
VERSION="11 (bullseye)"
VERSION_CODENAME=bullseye
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"
`
case strings.Contains(cmd, "free -h"):
return ` total used free shared buff/cache available
Mem: 1022Gi 12Gi 3.0Gi 1.0Gi 47Gi 480Gi
Swap: 0B 0B 0B
`
//---------------- 敏感文件诱导类 ----------------
case strings.Contains(cmd, "ls /home"):
return "admin backup devops secret\n"
case strings.Contains(cmd, "ls /var/log"):
return `auth.log apache2 payment_system.log database_backup.log
`
case strings.Contains(cmd, "ls"):
return "password.txt\n"
case strings.Contains(cmd, "cat /etc/passwd"):
return `root:x:0:0:root:/root:/bin/bash
admin:x:1000:1000:,,,:/home/admin:/bin/bash
mysql:x:106:113:MySQL Server,,,:/nonexistent:/bin/false
core:x:0:0:,,,:/root:/bin/bash
`
//---------------- 网络配置诱导类 ----------------
case strings.Contains(cmd, "ifconfig") || strings.Contains(cmd, "ip addr"):
return `eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.1.105 netmask 255.255.255.0 broadcast 192.168.1.255
inet6 fe80::250:56ff:fec0:8888 prefixlen 64 scopeid 0x20<link>
ether 00:50:56:c0:88:88 txqueuelen 1000 (Ethernet)
RX packets 123456 bytes 123456789 (117.7 MiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 98765 bytes 9876543 (9.4 MiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
loop txqueuelen 1000 (Local Loopback)
`
case strings.Contains(cmd, "netstat -antp"):
return `Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 1234/sshd
tcp 0 0 192.168.1.105:5432 203.0.113.5:43892 ESTABLISHED 5678/postgres
tcp6 0 0 :::8080 :::* LISTEN 91011/java
`
//---------------- 凭证钓鱼类 ----------------
case strings.Contains(cmd, "mysql -u root -p"):
return "ERROR 1045 (28000): Access denied\n"
case strings.Contains(cmd, "sudo -l"):
return `Matching Defaults entries for admin on this host:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin
User admin may run the following commands on honeypot:
(ALL) NOPASSWD: /usr/bin/vim /etc/shadow
`
//---------------- 进程服务类 ----------------
case strings.Contains(cmd, "ps aux"):
return `USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 169020 13184 ? Ss May01 0:12 /sbin/init
admin 1234 0.3 2.1 1123456 178912 ? Sl May01 12:34 /opt/payment_system/payment_processor --debug
`
//---------------- 定制化陷阱 ----------------
case strings.Contains(cmd, "find / -name *.db"):
return `/var/lib/mysql/transactions.db
/home/backup/internal_users.db
`
case strings.Contains(cmd, "curl"):
return `<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>404 Not Found</title>
</head><body>
<h1>Not Found</h1>
<p>The requested URL was not found on this server.</p>
</body></html>
`
default:
// 模糊响应增加真实感
if rand.Intn(100) > 70 { // 30%概率返回"command not found"
return "sh: command not found: " + strings.Fields(cmd)[0] + "\n"
}
return "\n"
}
}