Skip to content

Commit 1bfb43a

Browse files
authored
Merge pull request rapid7#20077 from adfoster-r7/update-haraka-module-to-work-with-newer-python-versions
Update haraka module to work with newer python versions
2 parents b74860a + da8e9e1 commit 1bfb43a

File tree

2 files changed

+351
-2
lines changed

2 files changed

+351
-2
lines changed
Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
#
2+
# distutils/version.py
3+
#
4+
# Implements multiple version numbering conventions for the
5+
# Python Module Distribution Utilities.
6+
#
7+
#
8+
# $Id$
9+
# Original source: https://github.com/python/cpython/blob/558e27abf1f1e0f87262238bc1d9e84cca7743c6/Lib/distutils/version.py#L93
10+
#
11+
12+
"""Provides classes to represent module version numbers (one class for
13+
each style of version numbering). There are currently two such classes
14+
implemented: StrictVersion and LooseVersion.
15+
16+
Every version number class implements the following interface:
17+
* the 'parse' method takes a string and parses it to some internal
18+
representation; if the string is an invalid version number,
19+
'parse' raises a ValueError exception
20+
* the class constructor takes an optional string argument which,
21+
if supplied, is passed to 'parse'
22+
* __str__ reconstructs the string that was passed to 'parse' (or
23+
an equivalent string -- ie. one that will generate an equivalent
24+
version number instance)
25+
* __repr__ generates Python code to recreate the version number instance
26+
* _cmp compares the current instance with either another instance
27+
of the same class or a string (which will be parsed to an instance
28+
of the same class, thus must follow the same rules)
29+
"""
30+
31+
import re
32+
33+
class Version:
34+
"""Abstract base class for version numbering classes. Just provides
35+
constructor (__init__) and reproducer (__repr__), because those
36+
seem to be the same for all version numbering classes; and route
37+
rich comparisons to _cmp.
38+
"""
39+
40+
def __init__ (self, vstring=None):
41+
if vstring:
42+
self.parse(vstring)
43+
44+
def __repr__ (self):
45+
return "%s ('%s')" % (self.__class__.__name__, str(self))
46+
47+
def __eq__(self, other):
48+
c = self._cmp(other)
49+
if c is NotImplemented:
50+
return c
51+
return c == 0
52+
53+
def __lt__(self, other):
54+
c = self._cmp(other)
55+
if c is NotImplemented:
56+
return c
57+
return c < 0
58+
59+
def __le__(self, other):
60+
c = self._cmp(other)
61+
if c is NotImplemented:
62+
return c
63+
return c <= 0
64+
65+
def __gt__(self, other):
66+
c = self._cmp(other)
67+
if c is NotImplemented:
68+
return c
69+
return c > 0
70+
71+
def __ge__(self, other):
72+
c = self._cmp(other)
73+
if c is NotImplemented:
74+
return c
75+
return c >= 0
76+
77+
78+
# Interface for version-number classes -- must be implemented
79+
# by the following classes (the concrete ones -- Version should
80+
# be treated as an abstract class).
81+
# __init__ (string) - create and take same action as 'parse'
82+
# (string parameter is optional)
83+
# parse (string) - convert a string representation to whatever
84+
# internal representation is appropriate for
85+
# this style of version numbering
86+
# __str__ (self) - convert back to a string; should be very similar
87+
# (if not identical to) the string supplied to parse
88+
# __repr__ (self) - generate Python code to recreate
89+
# the instance
90+
# _cmp (self, other) - compare two version numbers ('other' may
91+
# be an unparsed version string, or another
92+
# instance of your version class)
93+
94+
95+
class StrictVersion (Version):
96+
97+
"""Version numbering for anal retentives and software idealists.
98+
Implements the standard interface for version number classes as
99+
described above. A version number consists of two or three
100+
dot-separated numeric components, with an optional "pre-release" tag
101+
on the end. The pre-release tag consists of the letter 'a' or 'b'
102+
followed by a number. If the numeric components of two version
103+
numbers are equal, then one with a pre-release tag will always
104+
be deemed earlier (lesser) than one without.
105+
106+
The following are valid version numbers (shown in the order that
107+
would be obtained by sorting according to the supplied cmp function):
108+
109+
0.4 0.4.0 (these two are equivalent)
110+
0.4.1
111+
0.5a1
112+
0.5b3
113+
0.5
114+
0.9.6
115+
1.0
116+
1.0.4a3
117+
1.0.4b1
118+
1.0.4
119+
120+
The following are examples of invalid version numbers:
121+
122+
1
123+
2.7.2.2
124+
1.3.a4
125+
1.3pl1
126+
1.3c4
127+
128+
The rationale for this version numbering system will be explained
129+
in the distutils documentation.
130+
"""
131+
132+
version_re = re.compile(r'^(\d+) \. (\d+) (\. (\d+))? ([ab](\d+))?$',
133+
re.VERBOSE | re.ASCII)
134+
135+
136+
def parse (self, vstring):
137+
match = self.version_re.match(vstring)
138+
if not match:
139+
raise ValueError("invalid version number '%s'" % vstring)
140+
141+
(major, minor, patch, prerelease, prerelease_num) = \
142+
match.group(1, 2, 4, 5, 6)
143+
144+
if patch:
145+
self.version = tuple(map(int, [major, minor, patch]))
146+
else:
147+
self.version = tuple(map(int, [major, minor])) + (0,)
148+
149+
if prerelease:
150+
self.prerelease = (prerelease[0], int(prerelease_num))
151+
else:
152+
self.prerelease = None
153+
154+
155+
def __str__ (self):
156+
157+
if self.version[2] == 0:
158+
vstring = '.'.join(map(str, self.version[0:2]))
159+
else:
160+
vstring = '.'.join(map(str, self.version))
161+
162+
if self.prerelease:
163+
vstring = vstring + self.prerelease[0] + str(self.prerelease[1])
164+
165+
return vstring
166+
167+
168+
def _cmp (self, other):
169+
if isinstance(other, str):
170+
other = StrictVersion(other)
171+
elif not isinstance(other, StrictVersion):
172+
return NotImplemented
173+
174+
if self.version != other.version:
175+
# numeric versions don't match
176+
# prerelease stuff doesn't matter
177+
if self.version < other.version:
178+
return -1
179+
else:
180+
return 1
181+
182+
# have to compare prerelease
183+
# case 1: neither has prerelease; they're equal
184+
# case 2: self has prerelease, other doesn't; other is greater
185+
# case 3: self doesn't have prerelease, other does: self is greater
186+
# case 4: both have prerelease: must compare them!
187+
188+
if (not self.prerelease and not other.prerelease):
189+
return 0
190+
elif (self.prerelease and not other.prerelease):
191+
return -1
192+
elif (not self.prerelease and other.prerelease):
193+
return 1
194+
elif (self.prerelease and other.prerelease):
195+
if self.prerelease == other.prerelease:
196+
return 0
197+
elif self.prerelease < other.prerelease:
198+
return -1
199+
else:
200+
return 1
201+
else:
202+
assert False, "never get here"
203+
204+
# end class StrictVersion
205+
206+
207+
# The rules according to Greg Stein:
208+
# 1) a version number has 1 or more numbers separated by a period or by
209+
# sequences of letters. If only periods, then these are compared
210+
# left-to-right to determine an ordering.
211+
# 2) sequences of letters are part of the tuple for comparison and are
212+
# compared lexicographically
213+
# 3) recognize the numeric components may have leading zeroes
214+
#
215+
# The LooseVersion class below implements these rules: a version number
216+
# string is split up into a tuple of integer and string components, and
217+
# comparison is a simple tuple comparison. This means that version
218+
# numbers behave in a predictable and obvious way, but a way that might
219+
# not necessarily be how people *want* version numbers to behave. There
220+
# wouldn't be a problem if people could stick to purely numeric version
221+
# numbers: just split on period and compare the numbers as tuples.
222+
# However, people insist on putting letters into their version numbers;
223+
# the most common purpose seems to be:
224+
# - indicating a "pre-release" version
225+
# ('alpha', 'beta', 'a', 'b', 'pre', 'p')
226+
# - indicating a post-release patch ('p', 'pl', 'patch')
227+
# but of course this can't cover all version number schemes, and there's
228+
# no way to know what a programmer means without asking him.
229+
#
230+
# The problem is what to do with letters (and other non-numeric
231+
# characters) in a version number. The current implementation does the
232+
# obvious and predictable thing: keep them as strings and compare
233+
# lexically within a tuple comparison. This has the desired effect if
234+
# an appended letter sequence implies something "post-release":
235+
# eg. "0.99" < "0.99pl14" < "1.0", and "5.001" < "5.001m" < "5.002".
236+
#
237+
# However, if letters in a version number imply a pre-release version,
238+
# the "obvious" thing isn't correct. Eg. you would expect that
239+
# "1.5.1" < "1.5.2a2" < "1.5.2", but under the tuple/lexical comparison
240+
# implemented here, this just isn't so.
241+
#
242+
# Two possible solutions come to mind. The first is to tie the
243+
# comparison algorithm to a particular set of semantic rules, as has
244+
# been done in the StrictVersion class above. This works great as long
245+
# as everyone can go along with bondage and discipline. Hopefully a
246+
# (large) subset of Python module programmers will agree that the
247+
# particular flavour of bondage and discipline provided by StrictVersion
248+
# provides enough benefit to be worth using, and will submit their
249+
# version numbering scheme to its domination. The free-thinking
250+
# anarchists in the lot will never give in, though, and something needs
251+
# to be done to accommodate them.
252+
#
253+
# Perhaps a "moderately strict" version class could be implemented that
254+
# lets almost anything slide (syntactically), and makes some heuristic
255+
# assumptions about non-digits in version number strings. This could
256+
# sink into special-case-hell, though; if I was as talented and
257+
# idiosyncratic as Larry Wall, I'd go ahead and implement a class that
258+
# somehow knows that "1.2.1" < "1.2.2a2" < "1.2.2" < "1.2.2pl3", and is
259+
# just as happy dealing with things like "2g6" and "1.13++". I don't
260+
# think I'm smart enough to do it right though.
261+
#
262+
# In any case, I've coded the test suite for this module (see
263+
# ../test/test_version.py) specifically to fail on things like comparing
264+
# "1.2a2" and "1.2". That's not because the *code* is doing anything
265+
# wrong, it's because the simple, obvious design doesn't match my
266+
# complicated, hairy expectations for real-world version numbers. It
267+
# would be a snap to fix the test suite to say, "Yep, LooseVersion does
268+
# the Right Thing" (ie. the code matches the conception). But I'd rather
269+
# have a conception that matches common notions about version numbers.
270+
271+
class LooseVersion (Version):
272+
273+
"""Version numbering for anarchists and software realists.
274+
Implements the standard interface for version number classes as
275+
described above. A version number consists of a series of numbers,
276+
separated by either periods or strings of letters. When comparing
277+
version numbers, the numeric components will be compared
278+
numerically, and the alphabetic components lexically. The following
279+
are all valid version numbers, in no particular order:
280+
281+
1.5.1
282+
1.5.2b2
283+
161
284+
3.10a
285+
8.02
286+
3.4j
287+
1996.07.12
288+
3.2.pl0
289+
3.1.1.6
290+
2g6
291+
11g
292+
0.960923
293+
2.2beta29
294+
1.13++
295+
5.5.kw
296+
2.0b1pl0
297+
298+
In fact, there is no such thing as an invalid version number under
299+
this scheme; the rules for comparison are simple and predictable,
300+
but may not always give the results you want (for some definition
301+
of "want").
302+
"""
303+
304+
component_re = re.compile(r'(\d+ | [a-z]+ | \.)', re.VERBOSE)
305+
306+
def __init__ (self, vstring=None):
307+
if vstring:
308+
self.parse(vstring)
309+
310+
311+
def parse (self, vstring):
312+
# I've given up on thinking I can reconstruct the version string
313+
# from the parsed tuple -- so I just store the string here for
314+
# use by __str__
315+
self.vstring = vstring
316+
components = [x for x in self.component_re.split(vstring)
317+
if x and x != '.']
318+
for i, obj in enumerate(components):
319+
try:
320+
components[i] = int(obj)
321+
except ValueError:
322+
pass
323+
324+
self.version = components
325+
326+
327+
def __str__ (self):
328+
return self.vstring
329+
330+
331+
def __repr__ (self):
332+
return "LooseVersion ('%s')" % str(self)
333+
334+
335+
def _cmp (self, other):
336+
if isinstance(other, str):
337+
other = LooseVersion(other)
338+
elif not isinstance(other, LooseVersion):
339+
return NotImplemented
340+
341+
if self.version == other.version:
342+
return 0
343+
if self.version < other.version:
344+
return -1
345+
if self.version > other.version:
346+
return 1
347+
348+
349+
# end class LooseVersion

modules/exploits/linux/smtp/haraka.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
import smtplib
1313
import re
14-
from distutils.version import StrictVersion
1514
from email.mime.application import MIMEApplication
1615
from email.mime.multipart import MIMEMultipart
1716
from email.mime.text import MIMEText
@@ -23,6 +22,7 @@
2322
except ImportError:
2423
from io import BytesIO
2524
from metasploit import module
25+
from metasploit.version import StrictVersion
2626

2727
metadata = {
2828
"name": "Haraka SMTP Command Injection",
@@ -162,7 +162,7 @@ def check_banner(args):
162162
c.quit()
163163

164164
if code == 220 and "Haraka" in banner:
165-
versions = re.findall("(\d+\.\d+\.\d+)", banner)
165+
versions = re.findall(r"(\d+\.\d+\.\d+)", banner)
166166
if versions:
167167
if StrictVersion(versions[0]) < StrictVersion("2.8.9"):
168168
return "appears"

0 commit comments

Comments
 (0)