企业FRP安全实践

2021-01-05
1044

一、序

1.1 前言

近期看到一则消息如下:

2020年11月17日上午11:56,某公司发出全员级别的通告。人工智能部AI实验室一名实习生私自将公司内网端口映射到公网,导致不法分子入侵公司服务器,违反《集团员工行为准则》和《员工信息安全规范》有关规定,解除其实习协议,并将相关涉案人员移送司法处理。
对此,该公司客服工作人员表示:“针对此情况我们没有收到相关部门的通知。我们会尝试与相关部门沟通,有进展的话会予以答复。”

看到的第一反应是无论此事真假,但如果发生在我司,安全部有没有能力去发现?于是,本着守望互助和发散思考的原则研究了一波内网端口映射到公网软件,恰好看到朋友圈已经有人总结了常见列表:

frp,proxychain,lcx,ngrok,regrok,ew,FPipe,Portmap,Termite,socat,natbypass,iox,abptts,Powercat,dnscat,reGeorg,tuna,reDuh,iodine,EarthWorm,sSocks,venom,s5.go

本人能力有限,故仅挑选一款倍受喜爱的frp来浅析一二,希望可以抛砖引玉。

文章核心内容如下:

两种常用模式的示例

如何提取特征进行安全检测

绕过安全检测的思路

1.2 frp简介

frp 是一个专注于内网穿透的高性能的反向代理应用,支持 TCP、UDP、HTTP、HTTPS 等多种协议。可以将内网服务以安全、便捷的方式通过具有公网 IP 节点的中转暴露到公网。

为什么使用 frp ?通过在具有公网 IP 的节点上部署 frp 服务端,可以轻松地将内网服务穿透到公网,同时提供诸多专业的功能特性,这包括:

客户端服务端通信支持 TCP、KCP 以及 Websocket 等多种协议。

采用 TCP 连接流式复用,在单个连接间承载更多请求,节省连接建立时间。

代理组间的负载均衡。

端口复用,多个服务通过同一个服务端端口暴露。

多个原生支持的客户端插件(静态文件查看,HTTP、SOCK5 代理等),便于独立使用 frp 客户端完成某些工作。

高度扩展性的服务端插件系统,方便结合自身需求进行功能扩展。

服务端和客户端 UI 页面。

二、两种常见示例

2.1 SSH 访问内网机器

1、图示

图示最能一眼直观看懂,奈何翻遍互联网没有找到让我满意的示意图,为清晰表达,故简单画图。

2、配置

frps配置

[common]
bind_port = 60000
log_file = /solo/secsoft/frp_0.34.3_linux_amd64/frps.log

frpc配置

[common]
server_addr = 115.159.54.230
server_port = 60000

[ssh]
type = tcp
local_ip = 127.0.0.1
local_port = 22
remote_port = 60001

客户端连接

ssh -oPort=60001 root@115.159.54.230

3、日志

frps日志,包含:启动、连接成功、退出成功

# 以下为启动
2020/11/24 15:24:37 [I] [service.go:190] frps tcp listen on 0.0.0.0:60000
2020/11/24 15:24:37 [I] [root.go:215] start frps success
# 以下为连接
2020/11/24 15:31:14 [I] [service.go:444] [545f544dbae98e7d] client login info: ip [36.112.46.206:58090] version [0.34.3] hostname [] os [linux] arch [amd64]
2020/11/24 15:31:14 [I] [tcp.go:63] [545f544dbae98e7d] [ssh] tcp proxy listen port [60001]
2020/11/24 15:31:14 [I] [control.go:446] [545f544dbae98e7d] new proxy [ssh] success
2020/11/24 15:31:18 [I] [proxy.go:103] [545f544dbae98e7d] [ssh] get a new work connection: [36.112.46.206:58090]
2020/11/24 15:32:19 [I] [proxy.go:103] [545f544dbae98e7d] [ssh] get a new work connection: [36.112.46.206:58090]
# 以下为断开客户端
2020/11/24 15:46:35 [I] [control.go:309] [545f544dbae98e7d] control writer is closing
2020/11/24 15:46:35 [I] [proxy.go:87] [545f544dbae98e7d] [ssh] proxy closing
2020/11/24 15:46:35 [I] [proxy.go:159] [545f544dbae98e7d] [ssh] listener is closed
2020/11/24 15:46:35 [I] [control.go:384] [545f544dbae98e7d] client exit success

frpc日志,只包含启动

[root@localhost frp_0.34.3_linux_amd64]# ./frpc -c ./frpc.ini
2020/11/24 15:24:49 [I] [service.go:288] [545f544dbae98e7d] login to server success, get run id [545f544dbae98e7d], server udp port [0]
2020/11/24 15:24:49 [I] [proxy_manager.go:144] [545f544dbae98e7d] proxy added: [ssh]
2020/11/24 15:24:49 [I] [control.go:180] [545f544dbae98e7d] [ssh] start proxy success

2.2 安全地暴露内网服务

1、图示

2、配置

frps配置

[common]
bind_port = 60000
log_file = /solo/secsoft/frp_SSH-2.0-OpenSSH_7.5_linux_amd64/frps.log
# trace, debug, info, warn, error
# 这里开启trace模式信息最全面,有助于对照wireshark抓包分析
log_level = trace

frpc内网服务器

[common]
server_addr = 115.159.54.230
server_port = 60000

[secret_ssh]
type = stcp
sk = hahahahaha
use_encryption = true
use_compression = true
local_ip = 127.0.0.1
local_port = 22

frpc客户端启动服务:

[common]
server_addr = 115.159.54.230
server_port = 60000

[secret_ssh_visitor]
type = stcp
role = visitor
server_name = secret_ssh
sk = hahahahaha
bind_addr = 127.0.0.1
bind_port = 7000

客户端连接

ssh -oPort=7000 root@127.0.0.1

3、日志

这里针对frps开启了trace模式,可以收到全面的信息,对于下文wireshark抓包分析、对照数据包分析指令等有帮助。

frps:

2020/11/25 19:30:28 [I] [service.go:190] frps tcp listen on 0.0.0.0:60000
2020/11/25 19:30:28 [I] [root.go:215] start frps success
2020/11/25 19:31:03 [T] [service.go:391] start check TLS connection...
2020/11/25 19:31:03 [T] [service.go:399] success check TLS connection
2020/11/25 19:31:03 [I] [service.go:444] [bcae207f693a5e10] client login info: ip [36.112.46.206:40466] version [SSH-2.0-OpenSSH_7.5] hostname [] os [unknow] arch [solosec]
2020/11/25 19:31:03 [I] [stcp.go:34] [bcae207f693a5e10] [secret_ssh] stcp proxy custom listen success
2020/11/25 19:31:03 [I] [control.go:446] [bcae207f693a5e10] new proxy [secret_ssh] success
2020/11/25 19:31:03 [D] [control.go:219] [bcae207f693a5e10] new work connection registered
2020/11/25 19:31:33 [D] [control.go:475] [bcae207f693a5e10] receive heartbeat
2020/11/25 19:32:03 [D] [control.go:475] [bcae207f693a5e10] receive heartbeat
2020/11/25 19:32:05 [T] [service.go:391] start check TLS connection...
2020/11/25 19:32:05 [T] [service.go:399] success check TLS connection
2020/11/25 19:32:05 [I] [service.go:444] [4d8da059a1c4e710] client login info: ip [106.120.247.188:44408] version [SSH-2.0-OpenSSH_7.5] hostname [] os [unknow] arch [solosec]
2020/11/25 19:32:05 [D] [control.go:219] [4d8da059a1c4e710] new work connection registered
2020/11/25 19:32:33 [D] [control.go:475] [bcae207f693a5e10] receive heartbeat
2020/11/25 19:32:35 [D] [control.go:475] [4d8da059a1c4e710] receive heartbeat

Frpc内网服务器:

2020/11/25 19:24:37 [I] [service.go:290] [bcae207f693a5e10] login to server success, get run id [bcae207f693a5e10], server udp port [0]
2020/11/25 19:24:37 [I] [proxy_manager.go:144] [bcae207f693a5e10] proxy added: [secret_ssh]
2020/11/25 19:24:37 [I] [control.go:180] [bcae207f693a5e10] [secret_ssh] start proxy success

frpc客户端启动服务:

2020/11/25 19:32:05 [I] [service.go:290] [4d8da059a1c4e710] login to server success, get run id [4d8da059a1c4e710], server udp port [0]
2020/11/25 19:32:05 [I] [visitor_manager.go:86] [4d8da059a1c4e710] start visitor success
2020/11/25 19:32:05 [I] [visitor_manager.go:130] [4d8da059a1c4e710] visitor added: [secret_ssh_visitor]

三、如何检测

3.1 针对第一种

1、特征提取

登陆后查看连接:

w查看有localhost登陆

确立连接中有到frps连接

确立连接中有到127.0.0.1:22的连接

根据端口反查:

[root@localhost ~]# lsof -i:55728
COMMAND   PID USER   FD   TYPE   DEVICE SIZE/OFF NODE NAME
frpc   27490 root   8u IPv4 11518331     0t0 TCP localhost:55728->localhost:ssh (ESTABLISHED)
sshd   30990 root   3u IPv4 11483050     0t0 TCP localhost:ssh->localhost:55728 (ESTABLISHED)

断开连接后查看:

frpc关闭服务后查看:

综上总结:

程序运行时有127.0.0.1:22的监听和网络连接

会有一定特征进程启动

未编译版本可通过指纹(md5)匹配

2、检测

“君子生非异也,善假于物也”,故这里借助多种产品来协助安全检测。

产品一:

程序运行时刷新资产状态,可以查看到frpc进程:

通过网络连接查看:

select datetime,agent_ip,cmd,src_ip,src_port,dst_ip,dst_port,pname from qtevent_net_connect where datatime > "now-2h" where pname like "frp"

通过进程查看:

select pid as 进程ID, processName as 进程名称, from_unixtime(startTime) as start_time, startArgs , md5 as MD5 from wx.wisteria_assets.linux_process where processName like "frp" order by start_time desc

通过md5检查(这里偷懒就对比了一下值):

产品二:

通过网络连接查看:

__topic__:aegis-log-network and ip:172.23.250.213 and frpc

通过进程查看:

__topic__:aegis-log-process and ip:172.23.250.213 and frpc

3.2 针对第二种

1、特征提取

这里只贴图,请按照上节内容自我思考。

2、检测

产品一:

通过网络连接查看:

select datetime,agent_ip,cmd,src_ip,src_port,dst_ip,dst_port,pname from qtevent_net_connect where datatime > "now-2h" where pname like "frp"

通过进程查看:

select pid as 进程ID, processName as 进程名称, from_unixtime(startTime) as start_time, startArgs , md5 as MD5 from wx.wisteria_assets.linux_process where processName like "frp" order by start_time desc

产品二:

通过网络连接查看:

__topic__:aegis-log-network and ip:172.23.250.213 and frpc

通过进程查看:

3.3 针对于客户端

流量分析类安全产品可以根据特征检测。

四、绕过思路

4.1 数据包特征分析

传统的流量分析安全设备是根据规则来命中事件类型,那么我们来研究一下数据包有什么特征。

抓取了一次从建立连接到ssh登陆的数据包,干干净净的话大概20个。

找到第一个传输flag信息的数据包,右键Follow--TCP Stream

先从数据包中分析有用的字段 version,hostname,os,arch,user,privilege_key,runid,metas,接下来会从代码萌新层面讨论如何去掉特征。

4.2 代码分析

git拉取源代码分析,git clone https://github.com/fatedier/frp

首先定位到pkg/msg/msg.go

type Login struct {
Version     string           `json:"version"`
Hostname     string           `json:"hostname"`
Os           string           `json:"os"`
Arch         string           `json:"arch"`
User         string           `json:"user"`
PrivilegeKey string           `json:"privilege_key"`
Timestamp   int64             `json:"timestamp"`
RunID       string           `json:"run_id"`
Metas       map[string]string `json:"metas"`

// Some global configures.
PoolCount int `json:"pool_count"`
}

type LoginResp struct {
Version       string `json:"version"`
RunID         string `json:"run_id"`
ServerUDPPort int   `json:"server_udp_port"`
Error         string `json:"error"`
}

先拿version字段为例,第二三段均为frps端收到连接时打log用的,所以不用考虑。

我们打开第一段会定位到client/service.go,再次跟进到pkg/util/version/version.go,代码如下

var version string = "0.34.3"

func Full() string {
return version
}

我这里修改版本号"0.34.3"为"SSH-2.0-OpenSSH_7.4"(一时脑洞,想着会不会被识别为ssh服务,哈哈)

这里还有个小插曲,就是修改了version后,建立连接时返回

"Please upgrade your frpc version to at least 0.18.0"

翻了一下代码定位到pkg/util/version/version.go中有如下:

func Compat(client string) (ok bool, msg string) {
if LessThan(client, "0.18.0") {
return false, "Please upgrade your frpc version to at least 0.18.0"
}
return true, ""
}

func LessThan(client string, server string) bool {
vc := Proto(client)
vs := Proto(server)
if vc > vs {
return false
} else if vc < vs {
return true
}
vc = Major(client)
vs = Major(server)
if vc > vs {
return false
} else if vc < vs {
return true
}
vc = Minor(client)
vs = Minor(server)
if vc > vs {
return false
} else if vc < vs {
return true
}
return false
}

LessThan函数中会去做一堆版本号比对,检测什么呀,直接把Compat返回值return true,或者把LessThan返回值return false都可以解决。

2020/11/25 10:07:45 [I] [service.go:444] [fd5d9a94278e35b7] client login info: ip [106.120.247.188:8103] version [0.34.3] hostname [] os [darwin] arch [amd64]

害,发现好多参数都是发了个寂寞~

既然如此,那么就顺便修改一下os和arch,用了个很low很简单方法

var newArch string = "solosec"
var newOs string = "unknow"
loginMsg := &msg.Login{
//Arch:     runtime.GOARCH,
Arch: newArch,
//Os:       runtime.GOOS,
Os:       newOs,
PoolCount: svr.cfg.PoolCount,
User:     svr.cfg.User,
Version:   version.Full(),
Timestamp: time.Now().Unix(),
RunID:     svr.runID,
Metas:     svr.cfg.Metas,
}

好了,编译打包。frp里面集成了打包shell,去到对应目录下执行package.sh,结果如下:

(new-env) ➜  frp git:(dev) ✗ ./package.sh       
go fmt ./...
env CGO_ENABLED=0 go build -ldflags "-s -w" -o bin/frps ./cmd/frps
env CGO_ENABLED=0 go build -ldflags "-s -w" -o bin/frpc ./cmd/frpc
build version: SSH-2.0-OpenSSH_7.4
env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-s -w" -o ./release/frpc_darwin_amd64 ./cmd/frpc
env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-s -w" -o ./release/frps_darwin_amd64 ./cmd/frps
env CGO_ENABLED=0 GOOS=freebsd GOARCH=386 go build -ldflags "-s -w" -o ./release/frpc_freebsd_386 ./cmd/frpc
env CGO_ENABLED=0 GOOS=freebsd GOARCH=386 go build -ldflags "-s -w" -o ./release/frps_freebsd_386 ./cmd/frps
env CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -ldflags "-s -w" -o ./release/frpc_freebsd_amd64 ./cmd/frpc
env CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -ldflags "-s -w" -o ./release/frps_freebsd_amd64 ./cmd/frps
env CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -ldflags "-s -w" -o ./release/frpc_linux_386 ./cmd/frpc
env CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -ldflags "-s -w" -o ./release/frps_linux_386 ./cmd/frps
env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o ./release/frpc_linux_amd64 ./cmd/frpc
env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o ./release/frps_linux_amd64 ./cmd/frps
env CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -ldflags "-s -w" -o ./release/frpc_linux_arm ./cmd/frpc
env CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -ldflags "-s -w" -o ./release/frps_linux_arm ./cmd/frps
env CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w" -o ./release/frpc_linux_arm64 ./cmd/frpc
env CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w" -o ./release/frps_linux_arm64 ./cmd/frps
env CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -ldflags "-s -w" -o ./release/frpc_windows_386.exe ./cmd/frpc
env CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -ldflags "-s -w" -o ./release/frps_windows_386.exe ./cmd/frps
env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-s -w" -o ./release/frpc_windows_amd64.exe ./cmd/frpc
env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-s -w" -o ./release/frps_windows_amd64.exe ./cmd/frps
env CGO_ENABLED=0 GOOS=linux GOARCH=mips64 go build -ldflags "-s -w" -o ./release/frpc_linux_mips64 ./cmd/frpc
env CGO_ENABLED=0 GOOS=linux GOARCH=mips64 go build -ldflags "-s -w" -o ./release/frps_linux_mips64 ./cmd/frps
env CGO_ENABLED=0 GOOS=linux GOARCH=mips64le go build -ldflags "-s -w" -o ./release/frpc_linux_mips64le ./cmd/frpc
env CGO_ENABLED=0 GOOS=linux GOARCH=mips64le go build -ldflags "-s -w" -o ./release/frps_linux_mips64le ./cmd/frps
env CGO_ENABLED=0 GOOS=linux GOARCH=mips GOMIPS=softfloat go build -ldflags "-s -w" -o ./release/frpc_linux_mips ./cmd/frpc
env CGO_ENABLED=0 GOOS=linux GOARCH=mips GOMIPS=softfloat go build -ldflags "-s -w" -o ./release/frps_linux_mips ./cmd/frps
env CGO_ENABLED=0 GOOS=linux GOARCH=mipsle GOMIPS=softfloat go build -ldflags "-s -w" -o ./release/frpc_linux_mipsle ./cmd/frpc
env CGO_ENABLED=0 GOOS=linux GOARCH=mipsle GOMIPS=softfloat go build -ldflags "-s -w" -o ./release/frps_linux_mipsle ./cmd/frps
/Users/solo/PycharmProjects/frp/release/packages
(new-env) ➜ frp git:(dev) ✗

好啦,这下子可以舒舒服服的绕过安全检测了吧

一边测试一边抓包看是否能绕过安全检测~

果不其然,还是检测出来了,打脸了

不哭不哭,我们来继续找原因,先看一下数据包

仔细翻了翻找到了刚刚没有看到的说明文档doc/server_plugin_zh.md,里面描述了用户登陆操作信息、创建代理的相关信息、心跳相关信息、新增 frpc 连接相关信息等数据字段格式,结合上图,突然有个扎眼的字段“privilege_key”,此时想了一下如果单独从前面几个字段的key去做特征误报率太高了,如果我设计就找几个特殊的key和几个特殊的value。心中定了想法,就去看一下这个key。

这里重点是第一个文件pkg/msg/msg.go,第二三个均是doc里面的内容,不用管。这里看了一下PrivilegeKey在pkg/auth/token.gopkg/auth/oidc.go都有关联,但是字段名却只有这个文件包含了3次,全局替换一波字段名,老样子编译打包一波。

随手一测~(不要问,需要结果的同学自己动手试一下就知道了)

五、思考

做一些发散思考和思路,时间原因,并未全部尝试;攻防本是对抗过程,文中方法可能随时失效,授人鱼不如授人以渔。

5.1 针对特征绕过的检测

上文中绕过是在建立连接时用户发送登录信息给服务端时,那么如果针对此检测可以有两个思路:

1、提取登录信息的新特征,防止正则匹配绕过

2、对前置或后置阶段特征提取,如发送RPC请求建立连接数据包、建立连接后的心跳数据包(最近读安全客有感,可参照引用和致谢[2])

5.2 NetFliter检测流量转发

NetFilter可以针对Linux包过滤、网络地址转换(NAT)和基于协议类型的连接跟踪,可以做多个HOOK点,我们可以抽象流量转发模型有以下特征:

数据包内容相同,但五元组不同

数据包间隔时间短

会在127.0.0.1上面做些文章

以上特征用代码实现用作入侵检测取证时很棒,有魄力的也可以做成服务检测报送式,用salt或ansible推送到服务器检测。

5.3 frps IP隐藏

设想,每次得手后都会在服务器留一个明文的.ini文件,应急响应和溯源时岂不是白白送上思路?

以上已有师傅实现,可参照引用和致谢[3]

5.4 补偿控制

frpc很多人会在BYOD终端启动,个人的测试中几款pc端的agent都无法从服务端采集到终端的frp软件信息和事件,故在没有更好的方法之前用一些手段去补偿控制,当然这些手段也需要不断的建设和运营,在阶段初期误报率和复杂度估计会很高。

服务器只允许堡垒机IP登录,其它ip做限制或登录告警(针对使用frp映射非ssh服务无法奏效)

抓取出互联网流量中心跳特征包(很多Saas服务会有心跳包,会产生大量误报,这个需要运营,如果流量分析产品可以实现则更佳)

零信任解决方案,基于账号、角色、权限、访问对象,结合微隔离、SDP,极端一些可以达到MAC(强制访问控制)模型

六、引用和致谢

感谢引用中各位大佬和团队的分享精神!frp是一个免费开源且在开发中的软件,我相信它会越来越好;各安全检测方法和测试手法也仅限于本人研究过的产品和特定版本,不能以偏概全。如果文章中有任何描述不正确或引用不当的地方,辛苦大佬们指正。

也欢迎各位安全同僚拍砖、交流和技术分享!微信:solosec或freebuf留言交流~

引用来源:

[1]https://gofrp.org/docs/frp官方文档

[2]https://mp.weixin.qq.com/s/f11pMe55aFR92NfH_VNgBg入侵检测系列1(上):基于私有协议的加密流量分析思路(Teamviewer篇)

[3]https://uknowsec.cn/posts/notes/FRP%E6%94%B9%E9%80%A0%E8%AE%A1%E5%88%92.htmlFRP改造计划

转载时必须以链接形式注明原始出处及本声明

扫描关注公众号