-1

I understand that relying on Python 2's built-in input() function (as opposed to raw_input(), which was renamed to input() in Python 3) is prone to security bugs. In this question, I'm trying to determine how to demonstrate this class of vulnerability.


Specifically, I'm trying to determine how to generate input to the code below which will cause "Access granted" to be emitted, without modifying the program itself, only by changing the value passed to stdin in response to the input() call.

import random
pass_true = input("What is the password?")
password = random.randint(0,2**30)

if password == pass_true:
    print("Access granted.")
else: 
    print ("Access denied.")

If the random number were generated before the input call (that is, if the second and third lines were switched), one could merely enter pass_true as the string, which would subsequently be evaluated to the value of the variable by that name -- but since the random number isn't yet known at the input() invocation, this approach doesn't work.

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • 1
    Put comments before the lines maybe – Bálint May 26 '17 at 17:56
  • 1
    First off, I don't think it's going to work because you're trying to compare a string to an integer. Use `password = str(random.randint(0,2**30))`. Secondly, you're trying to guess a number between 0 and 2^30..? – Pedro von Hertwig Batista May 26 '17 at 17:57
  • I think that @DeepSpace mis-identified the problem; that question doesn't seem to apply at all. I've voted to reopen. – Prune May 26 '17 at 17:58
  • What *is* the problem you're trying to solve. The code you posted runs just fine -- for what it actually *does*. The user has to guess a 9-digit random number, in advance. This is *not* a "password" situation, since the access code changes every time. Please clarify. I was going to vote to close this as "unclear what you're asking". – Prune May 26 '17 at 18:00
  • @CharlesDuffy I disagree. OP wants to know what they need to input in order for the program to print "Access granted", "It would have been really easy if the password was generated before the input but I and completely clueless as to how to approach this." They want to know before hand what the value of `password ` will be, but I'll reopen it anyway. – DeepSpace May 26 '17 at 18:01
  • 2
    You have failed to convey your problem – Amey Kumar Samala May 26 '17 at 18:01
  • @DeepSpace, yes, but presumably their only means for attacking it is injecting arbitrary values via `input()` -- no? It's fine that someone knows that it's possible to set an arbitrary seed *if* they can convince a given function to be called, but that isn't an answer unless they actually know how to trigger the call. – Charles Duffy May 26 '17 at 18:01
  • @PedrovonHertwig, it's only a string in Python 3. In Python 2, `input()` parses the value in the same manner in which it would parse other Python syntax, and thus coerces it to an integer. – Charles Duffy May 26 '17 at 18:02
  • @CharlesDuffy No, using the same seed will cause `randint` to generate the same numbers in every run of the program. All OP needs to do is to run the program once to know what `password` will be the next time. – DeepSpace May 26 '17 at 18:04
  • @DeepSpace, only if you edited it into the source code of the program. If the OP could edit the source code in an arbitrary way, this wouldn't be a question. – Charles Duffy May 26 '17 at 18:05
  • Obviously Python 2 -- otherwise, `password == pass_true` would always be falsey. – Charles Duffy May 26 '17 at 18:09
  • I can't understand your problem, but you should turn the password to a string **str()** or the input to a integer **int()** in order to check equality. – Ender Look May 26 '17 at 19:56
  • @EnderLook, the whole point of this is to find a demonstration of **why** using `input()` in Python 2 is dangerously buggy (in a security-impacting way). To be clear, passing 2 to `input()` returns an integer in Python 2, not a string as it does in Python 3, so no explicit cast is necessary. – Charles Duffy May 26 '17 at 19:58
  • @CharlesDuffy, Oh sorry, I use python 3. So, he have to use raw_input(), no? (I don't know much of python 2). – Ender Look May 26 '17 at 20:00
  • @EnderLook, yes, using `raw_input()` would close the security hole that this question asks how to demonstrate. – Charles Duffy May 26 '17 at 20:01
  • @DeepSpace, ...I'd be curious -- in your estimation, has this been edited to the point of being a clear and useful question? – Charles Duffy May 26 '17 at 20:03
  • @CharlesDuffyI suppose.. – DeepSpace May 26 '17 at 20:04

1 Answers1

6

Approach 1: Overriding random.randint() Completely

Assuming the interpreter is Python 2, the following input will cause the result of the comparison to be True, without modifying the program at all:

[42 for random.randint in [lambda x,y: 42]][0]

Witness the execution transcript:

$ python test.py
What is the password?[42 for random.randint in [lambda x,y: 42]][0]
Access granted.

Digression: How It Works

To explain this -- in Python 2, input() -- as opposed to raw_input() -- runs content entered by the user through an evaluation pass. This means code can access defined variables, or have side effects on them.

lambda x,y: 42

is a function that takes two arguments, and always returns 42.

[42 for random.randint in [lambda x,y: 42]]

is a list comprehension that goes through a list of items containing only that lambda expression, assigning each of them in turn to the variable random.randint, and then adding the value 42 to its list; tacking on a [0] on the end thus makes the end effect be an evaluation to that value.


Approach 2: Setting a Known Seed

[random.seed(1), random.randint(0, 2**30), random.seed(1)][1]

Again, the transcript:

$ python test.py
What is the password?[random.seed(1), random.randint(0, 2**30), random.seed(1)][1]
Access granted.

In this case, we caused the random number generator to be initialized with a known seed; generated a value from the same range; and then reinitialized from that seed again -- and return the second value from that list, corresponding with the first random number generated with the given range and the given seed.

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • @KDEx, does the explanation I've added help? – Charles Duffy May 26 '17 at 18:20
  • @CharlesDuffy you just taught me something! – Andrés Pérez-Albela H. May 26 '17 at 18:22
  • @CharlesDuffy is there any way to inject an import statement? I've tried with some input like exec but had no luck. – Andrés Pérez-Albela H. May 26 '17 at 18:45
  • The key here is the "leaky" scope in Python 2 list comprehsions. – juanpa.arrivillaga May 26 '17 at 19:17
  • 2
    @AndrésPérez-AlbelaH., it can be done -- `__import__('modulename')` is applicable. – Charles Duffy May 26 '17 at 19:29
  • 1
    @AndrésPérez-AlbelaH., ...so, if the OP's code didn't `import random` until after the `input()`, you could still exploit it with `[42 for __import__('random').randint in [lambda x,y: 42]][0]` – Charles Duffy May 26 '17 at 19:38
  • 1
    @juanpa.arrivillaga, indeed -- though that was just the first means of generating a side effect and also returning a chosen value from within an expression that came to mind. One could also use, for instance, `[setattr(random, 'randint', lambda x,y:42), 42][1]` with no list comprehension involved. Surely there are terser forms as well, but finding them would be a question for [codegolf.se]. – Charles Duffy May 26 '17 at 19:44
  • @CharlesDuffy `[42 for __import__('random').randint in [lambda x,y: 42]][0]` doesn't work for me – Andrés Pérez-Albela H. May 26 '17 at 19:49
  • @AndrésPérez-AlbelaH., which version of Python? I'm testing with 2.7.10. – Charles Duffy May 26 '17 at 19:49
  • @CharlesDuffy I'm getting *NameError: name 'random' is not defined* with `2.7.11` – Andrés Pérez-Albela H. May 26 '17 at 19:52
  • Probably not from the `input()` line, though -- I'm guessing you *removed* the `import` instead of *moving* it down to a later line (to happen only after the `input()`). The line *invoking* `random.randint()` still has to have a handle on the `random` module, after all. – Charles Duffy May 26 '17 at 19:53
  • 1
    @AndrésPérez-AlbelaH., ...that said, if you *really* want to add a handle on the module to local scope, you can do that too as a side effect: `[globals().__setitem__('random', __import__('random')), setattr(__import__('random'), 'randint', lambda x,y:42), 42][-1]` – Charles Duffy May 26 '17 at 19:57