Skip to content

Commit e32213b

Browse files
Merge pull request #1 from jukrieger/main
Add Cybersecurity Rumble Posts
2 parents 8769084 + d3dfec7 commit e32213b

File tree

2 files changed

+365
-0
lines changed

2 files changed

+365
-0
lines changed
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
---
2+
date: 2021-09-25
3+
author: Julian Krieger
4+
---
5+
6+
# RobertIsAGangsta
7+
8+
This CTF challenge is actually a three parter. It is a python file with the following contents relevant to the challenge:
9+
10+
```python
11+
def validate_command(string):
12+
return len(string) == 4 and string.index("date") == 0
13+
14+
def api_admin(data, user):
15+
if user is None:
16+
return error_msg("Not logged in")
17+
is_admin = get_userdb().is_admin(user["email"])
18+
if not is_admin:
19+
return error_msg("User is not Admin")
20+
21+
cmd = data["data"]["cmd"]
22+
# currently only "date" is supported
23+
if validate_command(cmd):
24+
out = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
25+
return success_msg(out.stdout.decode())
26+
27+
return error_msg("invalid command")
28+
29+
@app.route("/json_api", methods=["GET", "POST"])
30+
def json_api():
31+
user = get_user(request)
32+
if request.method == "POST":
33+
data = json.loads(request.get_data().decode())
34+
# print(data)
35+
action = data.get("action")
36+
if action is None:
37+
return "missing action"
38+
39+
return actions.get(action, api_error)(data, user)
40+
41+
else:
42+
return json.dumps(user)
43+
44+
def is_admin(self, email):
45+
user = self.db.get(email)
46+
if user is None:
47+
return False
48+
49+
# TODO check userid type etc
50+
return user["userid"] > 90000000
51+
52+
def api_create_account(data, user):
53+
dt = data["data"]
54+
email = dt["email"]
55+
password = dt["password"]
56+
groupid = dt["groupid"]
57+
userid = dt["userid"]
58+
activation = dt["activation"]
59+
60+
assert len(groupid) == 3
61+
assert len(userid) == 4
62+
63+
userid = json.loads("1" + groupid + userid)
64+
65+
def check_activation_code(activation_code):
66+
# no bruteforce
67+
time.sleep(20)
68+
if "{:0>4}".format(random.randint(0, 10000)) in activation_code:
69+
return True
70+
else:
71+
return False
72+
```
73+
74+
The target is a python webapp written in flask. Luckily, the organizers wrapped it in a docker container so we can run it ourselves.
75+
76+
Starting the docker container is fairly easy.
77+
78+
```bash
79+
docker build -t robertisagansta && docker run -it -p 5000:5000 robertisagangsta
80+
```
81+
82+
When visiting `localhost:5000`, we are greeted with a fairly basic entry screen.
83+
Naturally, our first instinct would be to register an account. We immediately notic a pretty bad delay when clicking the submit button.
84+
An info text gives us a hint: Trying to register an account has a built in delay of 20 seconds.
85+
After the 20 seconds are up, we are greeted with another info box: Creating our user fails, because we do not have the needed activation code.
86+
There is no further hint about what a correct information code would entail in the challenge's description. However, since we are in the posession
87+
of our webapp's source code, we can just look up how the activation code validation logic works!
88+
89+
```python
90+
def check_activation_code(activation_code):
91+
# no bruteforce
92+
time.sleep(20)
93+
if "{:0>4}".format(random.randint(0, 10000)) in activation_code:
94+
return True
95+
else:
96+
return False
97+
```
98+
99+
Huh, seems like the activation code is generated at random at runtime in form of a 4 digit number. We *could* try to brute force this number by sending the same request over and over and
100+
hoping that the RNG generates a match. The 20 second timer isn't much of as much of a showstopper as you might think. Even though the webapp is running in a single thread, something like 10000 requests should be handled pretty easy.
101+
There is a much smarter way though: Let's have a look at how `check_activation_code` is called. When we open the browsers developer tools to check what endpoint is called when we try to register a user, we can see
102+
that it sends a POST request to the route `http://localhost:5000/json_api`. This matches with the `json_api` function in `app.py`.
103+
104+
```python
105+
106+
@app.route("/json_api", methods=["GET", "POST"])
107+
def json_api():
108+
user = get_user(request)
109+
if request.method == "POST":
110+
data = json.loads(request.get_data().decode())
111+
# print(data)
112+
action = data.get("action")
113+
if action is None:
114+
return "missing action"
115+
116+
return actions.get(action, api_error)(data, user)
117+
118+
else:
119+
return json.dumps(user)
120+
```
121+
122+
`json_api` is pretty simple: First, it tries to get the current session's user instance. Then, it decodes the JSON in our POST request's body and loads it into a `data` dictionary.
123+
Our json data needs to include an `action` key with a value of `create_account` if we want to call the `api_create_account` function.
124+
We also need to include a `data` key with the data that is needed in `api_create_account`.
125+
126+
```python
127+
def api_create_account(data, user):
128+
dt = data["data"]
129+
email = dt["email"]
130+
password = dt["password"]
131+
groupid = dt["groupid"]
132+
userid = dt["userid"]
133+
activation = dt["activation"]
134+
135+
assert len(groupid) == 3
136+
assert len(userid) == 4
137+
138+
userid = json.loads("1" + groupid + userid)
139+
140+
if not check_activation_code(activation):
141+
return error_msg("Activation Code Wrong")
142+
# print("activation passed")
143+
144+
if get_userdb().add_user(email, userid, password):
145+
# print("user created")
146+
return success_msg("User Created")
147+
else:
148+
return error_msg("User creation failed")
149+
```
150+
151+
`api_create_account` has a single job: It checks if the activation code is valid and if it is, it creates our user in the database.
152+
Now, on to our first problem: How can we manipulate data so that the activation code matches what we need?
153+
There are actually two ways to solve this!
154+
155+
One way could be to write a small script that generates all numbers from `0000` to `9999` and concatenate them into a string.
156+
157+
```python
158+
import itertools
159+
list = map(lambda x: ''.join(map(str, x)), itertools.product(range(10), repeat=4)))
160+
print(map(lambda x: ''.join(x), list))
161+
```
162+
163+
This prints out all numbers into a gigantic string. We can copy that, paste it into the activation code input field and send it over to the server.
164+
If we do that and wait, we can see that our account has been successfully created!
165+
By the way, we could've also just copied `list` directly. An array of numbers is valid JSON, and `json.loads` wouldve turned the activation code entry
166+
into a list inside `json_api`. Since the `in` check in `check_activation_code` also works on lists, this would've worked as well.

0 commit comments

Comments
 (0)