Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 40 additions & 3 deletions lib/core/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import posixpath
import random
import re
import signal
import socket
import string
import subprocess
Expand Down Expand Up @@ -2476,22 +2477,58 @@ def getConsoleWidth(default=80):

return width or default

def shellExec(cmd):
def shellExec(cmd, timeout=None):
"""
Executes arbitrary shell command
Executes arbitrary shell command, optionally bounded by 'timeout' seconds - killing (and
flagging) a hung child instead of blocking forever, as callers otherwise have no other
watchdog around this call (e.g. --vuln-test runs one such call per entry, unattended)

>>> shellExec('echo 1').strip() == '1'
True
"""

retVal = ""
timedOut = []

try:
retVal = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT).communicate()[0] or ""
popenKwargs = {"shell": True, "stdout": subprocess.PIPE, "stderr": subprocess.STDOUT}
if timeout:
# shell=True's Popen.pid is the shell, not the (possibly grandchild) command it runs -
# killing just that pid leaves the real child holding the stdout pipe open, so
# communicate() keeps blocking past the deadline; run it in its own group/session instead
# so the whole tree can be killed at once
if IS_WIN:
popenKwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
else:
popenKwargs["preexec_fn"] = os.setsid

process = subprocess.Popen(cmd, **popenKwargs)

def _kill():
timedOut.append(True)
try:
if IS_WIN:
subprocess.call(["taskkill", "/F", "/T", "/PID", str(process.pid)])
else:
os.killpg(os.getpgid(process.pid), signal.SIGKILL)
except Exception:
pass

timer = threading.Timer(timeout, _kill) if timeout else None
if timer:
timer.daemon = True
timer.start()

retVal = process.communicate()[0] or ""

if timer:
timer.cancel()
except Exception as ex:
retVal = getSafeExString(ex)
finally:
retVal = getText(retVal)
if timedOut:
retVal += "\n[shellExec] child process tree killed after exceeding %d-second timeout" % timeout

return retVal

Expand Down
2 changes: 1 addition & 1 deletion lib/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from thirdparty import six

# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
VERSION = "1.10.9.24"
VERSION = "1.10.9.25"
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)
Expand Down
4 changes: 3 additions & 1 deletion lib/core/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,9 @@ def _thread():

os.environ["SQLMAP_UNSAFE_EVAL"] = '1'

output = shellExec(cmd)
# bounded well above the slowest known entry (GraphQL, ~96s) - a hung entry fails fast and
# visibly instead of silently burning the whole CI job's timeout (see #6129 CI investigation)
output = shellExec(cmd, timeout=180)

if not all((check in output if not check.startswith('~') else check[1:] not in output) for check in checks) or "unhandled exception" in output:
dataToStdout("---\n\n$ %s\n" % cmd)
Expand Down