Skip to content
Permalink
Browse files Browse the repository at this point in the history
Fix: Dailymotion team (Nicolas Perraud) found a way to bypass pickle.…
…loads protection and execute code from daemon if you can exchange with it's internal port. Now we whitelist allowed class only :)

Bonus: update the changelog, Thanks & setup.py so we can release soon :)
  • Loading branch information
naparuba committed Feb 3, 2022
1 parent 8163d64 commit 2dae40f
Show file tree
Hide file tree
Showing 5 changed files with 204 additions and 26 deletions.
48 changes: 47 additions & 1 deletion Changelog
Expand Up @@ -3,14 +3,60 @@ Shinken ChangeLog
########################


3.0.03 - X/X/2017
2.4.4 - XX/02/2022
-----------------
CORE ENHANCEMENT
Enh: (backport from Enterprise, internal ref #SEF-202) Big boost for the broker daemons, especially for very large setups and slow modules (yes Graphite, I'm looking at you).
Enh: (backport from Enterprise ref:#SEF-103) Slight boost of startup time by remiving useless hash computation.
Enh: (Christophe Simon) Force problem/impact evaluation
Enh: (Christophe Simon) added strict object name conflict policy
Enh: (Christophe Simon) better objects balancing between schedulers (#1999)
Enh: (Christophe Simon) Maintenance checks (#1929)
Enh: (Nicolas DUPEUX) Add an option to dump configuration as build by shinken to a json file (#1954)
Enh: (Lionel Sausin) Add instructions to use --install-scripts in pip (#1890)
Enh: (Christophe Simon) extend duplicate_foreach to host/hostgroups (#1905)
Enh: (Christophe Simon) Made memory free an opt-in option
Enh: (Christophe Simon) Explicitly frees memory when receiving new conf
Enh: (Christophe Simon) Harmonized graceful restart
Enh: (David Durieux) Add support of proxy socks5 for shinken cli (#1583)
Enh: (Mateusz Korniak) Add info about exit error code when check finished with error/was signalled. (#1766)

CORE FIXES
Fix: (backport from Enterprise ref:#SEF-76) In some case, we can have "negative" value for downtime depth, and then the element will never exit from downtime.
Fix: Dailymotion team (Nicolas Perraud) found a way to bypass pickle.loads protection and execute code from daemon if you can exchange with it's internal port. Now we whitelist allowed class only :)
Fix: (Christophe Simon) Enforced downtime state after retention load
Fix: (V. D'AGOSTINO) Update about.rst to fix a typo
Fix: (Flavien Peyre) multiple notification way when using a contact template (#1867)
Fix: (Dani Rodríguez) Update typographic errors and explain things in the help messages (#1968)
Fix: (Dani Rodríguez) brok queues not producing broks (#1971)
Fix: (David Gil) Pin CherryPy dependpency < 9.0.0 (#1983)
Fix: (Vladimir Kazarin) Update dmz-monitoring.rst (#1980)
Fix: (Muhammad Zeeshan Qazi) #1961 : Replace #!/usr/bin/python to #!/usr/bin/python2 (#1979)
Fix: (Dani Rodríguez) Fixes calls to cProfile in shinken binaries (#1967)
Fix: (wilfriedroset) shinken typo (#1974)
Fix: (efficks) CherryPy >= 3 required (#1943)
Fix: (David Gil) osmatch in nmap discovery process (#1944)
Fix: (Konstantin Shalygin) modules_manager: ignore '.git' dir when load modules. (#1883)
Fix: (Christophe Simon) http_client exceptions management
Fix: (Christophe Simon) OSX (darwin) platform detection
Fix: (Christophe Simon) unicode string parisng in shinken cmdline
Fix: (Olivier Hanesse) missing ini file for shinken.io
Fix: (Olivier Hanesse) #1857 Set default value for proxy/proxy_socks5 for shinken cli
Fix: (Olivier Hanesse) #1845 Add missing paramter for statsd
Fix: (Nicolas Le Manchet) Improve systemd units reliability
Fix: (Frédéric MOHIER) Iterate correctly in the Services object (#1842)


MISC FIXES
Fix: (Yann 'Ze' Richard) missing email header to prevent "Out Of Office" messages (#2014)
Fix: (se4598*) #1951: Email notification in text mode (#1986)
Enh: (Arthur Lutz) starttls and username/password for SMTP
Fix: (George Shuklin) trace on sening smtp messages
Doc: (rdmo) than -> as
Doc: (dodofox) specify host inheritance for contact/contactgroup (#1872)
Doc: (Elliott Peay) Fix default reactionner port in docs
Fix: (Konstantin Shalygin) Encode attachment. (#1817)
Fix: (Marc Remy) Typo: Update some doc files. (#1821)


2.4.3 - 10/03/2016
Expand Down
1 change: 1 addition & 0 deletions THANKS
Expand Up @@ -111,3 +111,4 @@ To Mathieu Parent for his patch about Thruk doc
To Aina Rakotoson for his bug report about reactionner_tag logic fail, and his patch to fix about it
To jylenhofgfi for his bug report about shinken cli problems and | grep
To Alexandre Viau for his patch about passive unmanaged host broks generation
To Nicolas Perraud for his warn about a RCE in the daemon exchange
2 changes: 1 addition & 1 deletion setup.py
Expand Up @@ -411,7 +411,7 @@ def _error(msg):
required_pkgs = ['pycurl']
setup(
name="Shinken",
version="2.4.3",
version="2.4.4",
packages=find_packages(),
package_data={'': package_data},
description="Shinken is a monitoring framework compatible with Nagios configuration and plugins",
Expand Down
89 changes: 78 additions & 11 deletions shinken/safepickle.py
Expand Up @@ -26,29 +26,96 @@
from cStringIO import StringIO



# Unpickle but strip and remove all __reduce__ things
# so we don't allow external code to be executed
# Unpickle but strip and remove all __reduce__ things so we don't allow external code to be executed
# Code from Graphite::carbon project
class SafeUnpickler(object):
PICKLE_SAFE = {
'copy_reg': set(['_reconstructor']),
'__builtin__': set(['object', 'set']),
'copy_reg' : set(['_reconstructor']),
'__builtin__' : set(['object', 'set']),

# Be sure to white list all we need to pickle.loads so user cannot exploit other modules (like bottle ^^)
'shinken.acknowledge' : set(['Acknowledge']),
'shinken.basemodule' : set(['BaseModule']),
'shinken.borg' : set(['Borg']),
'shinken.check' : set(['Check']),
'shinken.brok' : set(['Brok']),
'shinken.commandcall' : set(['CommandCall']),
'shinken.comment' : set(['Comment']),
'shinken.complexexpression' : set(['ComplexExpressionNode']),
'shinken.contactdowntime' : set(['ContactDowntime']),
'shinken.daterange' : set(['Timerange',
'Daterange',
'CalendarDaterange',
'StandardDaterange',
'MonthWeekDayDaterange',
'MonthDateDaterange',
'WeekDayDaterange',
'MonthDayDaterange',
]),
'shinken.dependencynode' : set(['DependencyNode']),
'shinken.downtime' : set(['Downtime']),
'shinken.discovery.discoverymanager' : set(['DiscoveredHost']),
'shinken.eventhandler' : set(['EventHandler']),
'shinken.external_command' : set(['ExternalCommand']),
'shinken.graph' : set(['Graph']),
'shinken.message' : set(['Message']),
'shinken.modulesctx' : set(['ModulesContext']),
'shinken.modulesmanager' : set(['ModulesManager']),
'shinken.notification' : set(['Notification']),
'shinken.objects.command' : set(['DummyCommand', 'Command', 'Commands']),
'shinken.objects.arbiterlink' : set(['ArbiterLink', 'ArbiterLinks']),
'shinken.objects.businessimpactmodulation': set(['Businessimpactmodulation', 'Businessimpactmodulations']),
'shinken.objects.brokerlink' : set(['BrokerLink', 'BrokerLinks']),
'shinken.objects.checkmodulation' : set(['CheckModulation', 'CheckModulations']),
'shinken.objects.config' : set(['Config']),
'shinken.objects.contact' : set(['Contact', 'Contacts']),
'shinken.objects.contactgroup' : set(['Contactgroup', 'Contactgroups']),
'shinken.objects.discoveryrule' : set(['Discoveryrule', 'Discoveryrules']),
'shinken.objects.discoveryrun' : set(['Discoveryrun', 'Discoveryruns']),
'shinken.objects.escalation' : set(['Escalation', 'Escalations']),
'shinken.objects.hostdependency' : set(['Hostdependency', 'Hostdependencies']),
'shinken.objects.host' : set(['Host', 'Hosts']),
'shinken.objects.hostescalation' : set(['Hostescalation', 'Hostescalations']),
'shinken.objects.itemgroup' : set(['Itemgroup', 'Itemgroups']),
'shinken.objects.hostgroup' : set(['Hostgroup', 'Hostgroups']),
'shinken.objects.hostextinfo' : set(['HostExtInfo', 'HostsExtInfo']),
'shinken.objects.item' : set(['Item', 'Items']),
'shinken.objects.macromodulation' : set(['MacroModulation', 'MacroModulations']),
'shinken.objects.matchingitem' : set(['MatchingItem']),
'shinken.objects.pack' : set(['Pack', 'Packs']),
'shinken.objects.notificationway' : set(['NotificationWay', 'NotificationWays']),
'shinken.objects.module' : set(['Module', 'Modules']),
'shinken.objects.pollerlink' : set(['PollerLink', 'PollerLinks']),
'shinken.objects.reactionnerlink' : set(['ReactionnerLink', 'ReactionnerLinks']),
'shinken.objects.realm' : set(['Realm', 'Realms']),
'shinken.objects.receiverlink' : set(['ReceiverLink', 'ReceiverLinks']),
'shinken.objects.resultmodulation' : set(['Resultmodulation', 'Resultmodulations']),
'shinken.objects.satellitelink' : set(['SatelliteLink', 'SatelliteLinks']),
'shinken.objects.schedulingitem' : set(['SchedulingItem']),
'shinken.objects.schedulerlink' : set(['SchedulerLink', 'SchedulerLinks']),
'shinken.objects.service' : set(['Service', 'Services']),
'shinken.objects.servicedependency' : set(['Servicedependency', 'Servicedependencies']),
'shinken.objects.serviceescalation' : set(['Serviceescalation', 'Serviceescalations']),
'shinken.objects.serviceextinfo' : set(['ServiceExtInfo', 'ServicesExtInfo']),
'shinken.objects.servicegroup' : set(['Servicegroup', 'Servicegroups']),
'shinken.objects.timeperiod' : set(['Timeperiod', 'Timeperiods']),
'shinken.objects.trigger' : set(['Trigger', 'Triggers']),

}


@classmethod
def find_class(cls, module, name):
if module not in cls.PICKLE_SAFE and not module.startswith('shinken.'):
if module not in cls.PICKLE_SAFE:
raise ValueError('Attempting to unpickle unsafe module %s' % module)
__import__(module)
mod = sys.modules[module]
if not module.startswith('shinken.') and name not in cls.PICKLE_SAFE[module]:
if name not in cls.PICKLE_SAFE[module]:
raise ValueError('Attempting to unpickle unsafe class %s/%s' %
(module, name))
return getattr(mod, name)


@classmethod
def loads(cls, pickle_string):
pickle_obj = cPickle.Unpickler(StringIO(pickle_string))
Expand Down
90 changes: 77 additions & 13 deletions test/test_safe_pickle.py
Expand Up @@ -24,12 +24,15 @@


import cPickle as pickle
import sys

from shinken_test import *

from shinken.safepickle import SafeUnpickler


should_not_change = False


def fff(b):
global should_not_change
should_not_change = b
Expand All @@ -41,37 +44,98 @@ def __reduce__(self):


class TestSafePickle(ShinkenTest):

def setUp(self):
pass


def launch_safe_pickle(self, buf):
SafeUnpickler.loads(buf)


def test_safe_pickle(self):
global should_not_change

print "Creating payload"
print("Creating payload")
buf = pickle.dumps(SadPanda(), 0)
should_not_change = False
print "Payload", buf
#self.assertEqual('HARD', host.state_type)
print "Now loading payload"
print("Payload", buf)
# self.assertEqual('HARD', host.state_type)
print("Now loading payload")
pickle.loads(buf)
print should_not_change
self.assertTrue(should_not_change)

# reset and try our fix
should_not_change = False
#SafeUnpickler.loads(buf)


# SafeUnpickler.loads(buf)
def launch_safe_pickle():
SafeUnpickler.loads(buf)


self.assertRaises(ValueError, launch_safe_pickle)
print should_not_change
print (should_not_change)
self.assertFalse(should_not_change)


# Thanks to security team @Dailymotion, we did have a RCE that ook like a return into libc
# exploit: they are using the bottle._load code (that blindly __import__) so the injected
# code will finally be executed. And as it's shinken.webui.bottle, it's ok with the
# safe_pickle filter. Smart ^^
def test_safe_pickle_exploit_rce(self):
###### Phase 1: can be exploited
# Arrange
rce_path = '/rce_exploited'
if rce_path in sys.path:
sys.path.remove(rce_path)

payload = b"""cshinken.webui.bottlewebui
_load
(S'sys:path.append("%s")'
tR.""" % rce_path

# Act
print("Now loading payload")
pickle.loads(payload)

# Assert
self.assertTrue(rce_path in sys.path)

##### Phase 2: no more exploitable by calling the good one
# Arrange
sys.path.remove(rce_path)


# SafeUnpickler.loads(buf)
def launch_safe_pickle():
SafeUnpickler.loads(payload)


# Act
self.assertRaises(ValueError, launch_safe_pickle)

# Assert
self.assertTrue(rce_path not in sys.path)


# Thanks to security team @Dailymotion, we did have a RCE that ook like a return into libc
# exploit: they are using the bottle._load code (that blindly __import__) so the injected
# code will finally be executed. And as it's shinken.webui.bottle, it's ok with the
# safe_pickle filter. Smart ^^
def test_safe_pickle_exploit_rce_can_load(self):
###### Phase 1: can be exploited
# Arrange

payload = pickle.dumps(Brok('void', {}))

# Act
b = SafeUnpickler.loads(payload)

# Assert
self.assertTrue(isinstance(b, Brok))


if __name__ == '__main__':
unittest.main()

0 comments on commit 2dae40f

Please sign in to comment.