├── .coveragerc
├── .gitignore
├── Ideas.org
├── README.md
├── README.rst
├── c101ex
├── __init__.py
├── _version.py
├── cookies.py
└── test
│ ├── __init__.py
│ └── test_cookies.py
├── requirements-testing.txt
├── requirements.txt
├── setup.py
├── specs
└── rot13-cookies.rst
└── tox.ini
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | branch = True
3 | source =
4 | c101ex
5 |
6 | [report]
7 | exclude_lines =
8 | pragma: no cover
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.py[cod]
2 |
3 | # C extensions
4 | *.so
5 |
6 | # Packages
7 | *.egg
8 | *.egg-info
9 | dist
10 | build
11 | eggs
12 | parts
13 | bin
14 | var
15 | sdist
16 | develop-eggs
17 | .installed.cfg
18 | lib
19 | lib64
20 | __pycache__
21 |
22 | # Installer logs
23 | pip-log.txt
24 |
25 | # Unit test / coverage reports
26 | .coverage
27 | .tox
28 | nosetests.xml
29 |
30 | # Translations
31 | *.mo
32 |
33 | # Mr Developer
34 | .mr.developer.cfg
35 | .project
36 | .pydevproject
37 |
--------------------------------------------------------------------------------
/Ideas.org:
--------------------------------------------------------------------------------
1 | * Crypto to break
2 |
3 | ** CTR mode with small counter: either huge nonce or small block size
4 |
5 | ** Old SSLv2 machine, MITM the handshake, brute force the key
6 |
7 | ** BEAST/CRIME attack based on compression
8 | ** RSA without padding + weak public exponent, message sent to multiple people
9 | ** DSA with reused k
10 | *** Totally bogus: always uses same k
11 | *** Kinda bogus: poor PRNG
12 | ** Backdoor your own version of Dual_EC_DRBG
13 | ** PRNG based on timeofday, maybe pid, other shitty entropy sources
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ======
2 | c101ex
3 | ======
4 |
5 | These are the exercises for Crypto 101, the introductory course on
6 | cryptography.
7 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | ======================
2 | Crypto 101 Exercises
3 | ======================
4 |
5 | Please remember that a lot of this code is *intentionally* broken to
6 | illustrate a cryptographic flaw. Do not use it as an example of how to
7 | do things. It is rife with bad ideas, intentionally.
8 |
--------------------------------------------------------------------------------
/c101ex/__init__.py:
--------------------------------------------------------------------------------
1 | from ._version import __version__
2 | version = __version__
3 |
--------------------------------------------------------------------------------
/c101ex/_version.py:
--------------------------------------------------------------------------------
1 | __version__ = "0.0.1"
2 |
--------------------------------------------------------------------------------
/c101ex/cookies.py:
--------------------------------------------------------------------------------
1 | """
2 | A website that uses cookies to determine admin status. Poorly.
3 | """
4 | from functools import partial
5 | from merlyn.multiplexing import addToStore
6 | from merlyn.exercise import SolvableResourceMixin
7 | from string import ascii_letters
8 | from twisted.web import resource, server, template
9 |
10 |
11 | class Index(resource.Resource, SolvableResourceMixin):
12 | isLeaf = True
13 | cookieName = "soylentgreen"
14 | exerciseIdentifier = "rot13-cookies"
15 |
16 | def __init__(self, store):
17 | resource.Resource.__init__(self)
18 | SolvableResourceMixin.__init__(self, store)
19 |
20 |
21 | def render_GET(self, request):
22 | """Greets the user appropriately.
23 |
24 | """
25 | rawCookie = request.getCookie(self.cookieName)
26 | if rawCookie is None:
27 | name, isAdmin = None, False
28 | else:
29 | cookie = self._decryptCookie(rawCookie)
30 | contents = self._parseCookie(cookie)
31 | name = contents.get("name")
32 | isAdmin = contents.get("admin") == "1"
33 |
34 | if isAdmin:
35 | self.solveAndNotify(request)
36 |
37 | return self._render(request, name, isAdmin)
38 |
39 |
40 | def render_POST(self, request):
41 | """Registers a user, setting their cookie.
42 |
43 | """
44 | name, = request.args.get("name")
45 | name = "".join(x if x in ascii_letters else "" for x in name)
46 |
47 | rawCookie = self._encodeCookie({"name": name})
48 | cookie = self._encryptCookie(rawCookie)
49 | request.addCookie(self.cookieName, cookie)
50 |
51 | return self._render(request, name, False)
52 |
53 |
54 | def _render(self, request, name, isAdmin):
55 | indexTemplate = IndexTemplate(name, isAdmin)
56 | return template.renderElement(request, indexTemplate)
57 |
58 |
59 | def _encryptCookie(self, value):
60 | """Encrypts a string cookie.
61 |
62 | """
63 | return value.encode("rot13")
64 |
65 |
66 | def _decryptCookie(self, value):
67 | """Decrypts a string cookie.
68 |
69 | """
70 | return value.decode("rot13")
71 |
72 |
73 | def _encodeCookie(self, values):
74 | """Encodes a dictionary of values as a string.
75 |
76 | """
77 | pairs = ("{0}={1}".format(k, v) for k, v in values.iteritems())
78 | return "&".join(pairs)
79 |
80 |
81 | def _parseCookie(self, rawCookie):
82 | """Parses a decrypted cookie string into a dictionary.
83 |
84 | """
85 | return dict(pair.split("=") for pair in rawCookie.split("&"))
86 |
87 |
88 |
89 | templateString = """
90 |
91 |
92 | A website
93 |
94 |
95 |
96 |
97 | """
98 |
99 |
100 |
101 | class IndexTemplate(template.Element):
102 | loader = template.XMLString(templateString)
103 |
104 | def __init__(self, name, isAdmin):
105 | self.name = name
106 | self.isAdmin = isAdmin
107 |
108 |
109 | @template.renderer
110 | def message(self, request, tag):
111 | message = "You are {name} and you are {status}."
112 |
113 | if self.name is None:
114 | name = "unregistered"
115 | else:
116 | name = self.name
117 |
118 | if self.isAdmin:
119 | status = "an administrator. Yay! Congratulations! You did it"
120 | else:
121 | status = "not an administrator"
122 |
123 | return tag(message.format(name=name, status=status))
124 |
125 |
126 | @template.renderer
127 | def registrationForm(self, request, tag):
128 | if self.name is not None: # Already registered
129 | return []
130 |
131 | label = template.tags.label("Name:")
132 | name = template.tags.input(type="text", name="name", id="name")
133 | submit = template.tags.input(type="submit", value="Register")
134 | return tag(label, name, submit)
135 |
136 |
137 |
138 | def makeSite(store):
139 | site = server.Site(Index(store))
140 | site.displayTracebacks = False
141 | return site
142 |
143 |
144 | addToStore = partial(addToStore,
145 | identifier="rot13-cookies-site",
146 | name="c101ex.cookies.makeSite")
147 |
--------------------------------------------------------------------------------
/c101ex/test/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/crypto101/exercises/43f41b63a9c3205ca72049b1e99a7719d7ba6ae7/c101ex/test/__init__.py
--------------------------------------------------------------------------------
/c101ex/test/test_cookies.py:
--------------------------------------------------------------------------------
1 | from c101ex import cookies
2 | from twisted.trial.unittest import SynchronousTestCase
3 | from twisted.web.template import flattenString
4 |
5 |
6 | class IndexTemplateTests(SynchronousTestCase):
7 | def setUp(self):
8 | self.body = None
9 |
10 |
11 | def _render(self, name, isAdmin):
12 | template = cookies.IndexTemplate(name, isAdmin)
13 | d = flattenString(None, template)
14 | self.body = self.successResultOf(d).lower()
15 |
16 |
17 | def assertNameEquals(self, name):
18 | self.assertIn("you are {name}".format(name=name), self.body)
19 |
20 |
21 | def assertIsAdministrator(self):
22 | self.assertIn("you are an administrator", self.body)
23 |
24 |
25 | def assertIsntAdministrator(self):
26 | self.assertIn("you are not an administrator", self.body)
27 |
28 |
29 | def hasForm(self):
30 | return "form" in self.body
31 |
32 |
33 | def test_unregistered(self):
34 | """An unregistered user is told they are unregistered and that they
35 | are not an administrator. There is a form for them to register.
36 |
37 | """
38 | self._render(None, False)
39 | self.assertNameEquals("unregistered")
40 | self.assertIsntAdministrator()
41 | self.assertTrue(self.hasForm())
42 |
43 |
44 | def test_user(self):
45 | """A user is greeted by their name and a notice that they are not an
46 | administrator. The registration form is no longer displayed.
47 |
48 | """
49 | self._render("lvh", False)
50 | self.assertNameEquals("lvh")
51 | self.assertIsntAdministrator()
52 | self.assertFalse(self.hasForm())
53 |
54 |
55 | def test_administrator(self):
56 | """An administrator is greeted by their name and a notice that they
57 | are an administrator. The registration form is no longer
58 | displayed.
59 |
60 | """
61 | self._render("ewa", True)
62 | self.assertNameEquals("ewa")
63 | self.assertIsAdministrator()
64 | self.assertFalse(self.hasForm())
65 |
66 |
67 | class ResourceTests(SynchronousTestCase):
68 | def setUp(self):
69 | self.resource = cookies.Index(None)
70 |
71 |
72 | def test_isLeaf(self):
73 | """The resource is a leaf.
74 |
75 | """
76 | self.assertTrue(self.resource.isLeaf)
77 |
78 |
79 | def test_siteTracebacksDisabled(self):
80 | """The site has tracebacks disabled.
81 |
82 | """
83 | site = cookies.makeSite(None)
84 | self.assertFalse(site.displayTracebacks)
85 |
86 |
87 | def test_endToEnd(self):
88 | """Users start out being not registered. Then, they can register,
89 | which sets a cookie with their name (with non-ASCII letters
90 | removed). They can no longer register again after that.
91 |
92 | """
93 | request = FakeRequest()
94 | self.resource.render_GET(request)
95 | self.assertTrue(request.finished)
96 | self.assertEqual(request.cookies, {})
97 |
98 | request = FakeRequest(args={"name": ["St=ring& Tr=ep$an#at!ion="]})
99 | self.resource.render_POST(request)
100 |
101 | self.assertTrue(request.finished)
102 | self.assertIn("StringTrepanation", request.body)
103 | self.assertIn("not an administrator", request.body)
104 | self.assertNotIn("form", request.body)
105 |
106 | rawCookie = request.cookies[self.resource.cookieName]
107 | cookie = self.resource._decryptCookie(rawCookie)
108 | data = self.resource._parseCookie(cookie)
109 | self.assertEqual(data["name"], "StringTrepanation")
110 |
111 |
112 | def test_renderNotAdmin(self):
113 | """Tries to render the template for a use that is not an
114 | administrator.
115 |
116 | Verifies that the registration form isn't sent, and that the
117 | exercise is not solved.
118 |
119 | """
120 | request = FakeRequest()
121 |
122 | self._solvedRequest = None
123 | self.resource.solveAndNotify = self._solveAndNotify
124 |
125 | rawCookie = self.resource._encodeCookie({"name": "lvh"})
126 | cookie = self.resource._encryptCookie(rawCookie)
127 | request.addCookie(self.resource.cookieName, cookie)
128 |
129 | self.resource.render_GET(request)
130 | self.assertTrue(request.finished)
131 | self.assertNotIn("you are an administrator", request.body)
132 | self.assertNotIn("form", request.body)
133 |
134 | self.assertIdentical(self._solvedRequest, None)
135 |
136 |
137 | def test_renderAdmin(self):
138 | """Tries to render the template for a use that is an administrator.
139 |
140 | Verifies that the registration form isn't sent, the welcome
141 | message says that the user is an administrator, and that the
142 | user has been notified of success.
143 |
144 | """
145 | request = FakeRequest()
146 |
147 | self._solvedRequest = None
148 | self.resource.solveAndNotify = self._solveAndNotify
149 |
150 | rawCookie = self.resource._encodeCookie({"name": "lvh", "admin": "1"})
151 | cookie = self.resource._encryptCookie(rawCookie)
152 | request.addCookie(self.resource.cookieName, cookie)
153 |
154 | self.resource.render_GET(request)
155 | self.assertTrue(request.finished)
156 | self.assertIn("you are an administrator", request.body)
157 | self.assertNotIn("form", request.body)
158 |
159 | self.assertIdentical(self._solvedRequest, request)
160 |
161 |
162 | def _solveAndNotify(self, request):
163 | """Fake solveAndNotify implementation that just remembers the request.
164 |
165 | """
166 | self._solvedRequest = request
167 |
168 |
169 |
170 | class FakeRequest(object):
171 | def __init__(self, args=None):
172 | self.args = args
173 | self.cookies = {}
174 | self.finished = False
175 | self.body = ""
176 |
177 |
178 | def getCookie(self, key):
179 | return self.cookies.get(key)
180 |
181 |
182 | def addCookie(self, key, value):
183 | self.cookies[key] = value
184 |
185 |
186 | def write(self, data):
187 | assert not self.finished
188 | self.body += data
189 |
190 |
191 | def finish(self):
192 | self.finished = True
193 |
--------------------------------------------------------------------------------
/requirements-testing.txt:
--------------------------------------------------------------------------------
1 | coverage
2 | pyroma
3 | pudb
4 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | twisted==19.2.1
2 | -e /Users/lvh/Code/Crypto101/merlyn
3 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from setuptools import setup, find_packages
3 | from setuptools.command.test import test as TestCommand
4 |
5 | packageName = "c101ex"
6 |
7 | import re
8 | versionLine = open("{}/_version.py".format(packageName), "rt").read()
9 | match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", versionLine, re.M)
10 | versionString = match.group(1)
11 |
12 | class Tox(TestCommand):
13 | def finalize_options(self):
14 | TestCommand.finalize_options(self)
15 | self.test_suite = True
16 |
17 | def run_tests(self):
18 | #import here, cause outside the eggs aren't loaded
19 | import tox
20 | sys.exit(tox.cmdline([]))
21 |
22 | setup(name=packageName,
23 | version=versionString,
24 | description='The Crypto 101 exercises.',
25 | long_description=open("README.rst").read(),
26 | url='https://github.com/lvh/Crypto101',
27 |
28 | author='Laurens Van Houtven',
29 | author_email='_@lvh.io',
30 |
31 | packages=find_packages(),
32 | test_suite=packageName + ".test",
33 |
34 | setup_requires=['tox'],
35 | cmdclass={'test': Tox},
36 | zip_safe=True,
37 |
38 | license='ISC',
39 | keywords="crypto twisted",
40 | classifiers=[
41 | "Development Status :: 3 - Alpha",
42 | "Framework :: Twisted",
43 | "Intended Audience :: Education",
44 | "License :: OSI Approved :: ISC License (ISCL)",
45 | "Programming Language :: Python :: 2 :: Only",
46 | "Programming Language :: Python :: 2.6",
47 | "Programming Language :: Python :: 2.7",
48 | "Topic :: Education",
49 | "Topic :: Games/Entertainment",
50 | "Topic :: Security :: Cryptography",
51 | ]
52 | )
53 |
--------------------------------------------------------------------------------
/specs/rot13-cookies.rst:
--------------------------------------------------------------------------------
1 | =======================================
2 | Website with ROT-13 "encrypted" cookies
3 | =======================================
4 |
5 | As a simple starter exercise, we'll attack a website that "encrypts"
6 | its cookies using ROT13.
7 |
8 | Cookie structure
9 | ================
10 |
11 | The plaintext cookies are key-value pairs. Keys are separated from
12 | values with ``=``, pairs are separated from each other with ``&``. For
13 | example::
14 |
15 | name=johnny&age=10&pet=rufus
16 |
17 | The goal of this exercise is to get admin access to the website. The
18 | website gives admin access to anyone who has an ``admin=1`` pair.
19 |
20 | ROT13
21 | =====
22 |
23 | ROT13 is a very simple encoding scheme that replaces every letter with
24 | one 13 letters further in the alphabet. Since the alphabet has 26
25 | letters, this has the interesting property that encrypting and
26 | decrypting is exactly the same operation. You can use this table to
27 | translate:
28 |
29 | abcdefghijklmnopqrstuvwxyz
30 | nopqrstuvwxyzabcdefghijklm
31 |
32 | All other bytes are unchanged.
33 |
34 | Since it doesn't involve any secret data, it doesn't really count as a
35 | form of encryption. People use it, for example, for giving out
36 | spoilers in a newsgroup for nethack, a video game. That way, people
37 | who want to read the spoiler have to go out of their way to read it;
38 |
39 | Attack walkthrough
40 | ==================
41 |
42 | Create a proxy connection to ``rot13-cookies-site`` (don't include the
43 | quotes). Connect to the port it tells you to. It's a web site, so a
44 | web browser is probably easiest. You'll be able to register. Once you
45 | do that, you get a cookie. Edit the cookie so it has the admin key
46 | pair in there, and see if it worked.
47 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py27
3 |
4 | [testenv]
5 | commands =
6 | pip install epsilon axiom pysqlite
7 | pip install \
8 | -r {toxinidir}/requirements.txt \
9 | -r {toxinidir}/requirements-testing.txt
10 |
11 | coverage run \
12 | {envdir}/bin/trial --temp-directory={envdir}/_trial \
13 | {posargs:c101ex}
14 |
15 | coverage report --show-missing
16 | coverage html --directory {envdir}/coverage
17 |
18 | pyroma .
19 |
--------------------------------------------------------------------------------