├── .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 | --------------------------------------------------------------------------------