Skip to content

Commit 980e0f1

Browse files
committed
Added Pixels Camp 2017 CTF W200 challenge
1 parent daa6b6b commit 980e0f1

40 files changed

+35687
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/src/venv
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
W200 - Regain Session
2+
==============
3+
4+
We know a site was hacked and every new account is quicky taken over, with the hackers changing the victims password. Try to re-gain access to your account.
5+
6+
7+
Installing
8+
----------
9+
10+
Requirements: `docker-compose`
11+
12+
Copy the whole folder contents to, for instance, `/opt/ctf/w200/`.
13+
14+
Then you just need to link the systemd service with `sudo ln -s /opt/ctf/w200/w200.service /etc/systemd/system/w200.service` and start it with `sudo systemdctl start w200.service`.
15+
16+
You can configure the flag on `src/app.py`, and which port the service is available in the `docker-compose.yml` file. By default it will listen in the `30200` port.
17+
18+
19+
20+
Known issues
21+
----------
22+
23+
Sometimes uwsgi drops connections, and some resources aren't loaded on the first time. Uwsgi config might need some tuning, or putting a nginx on front of uwsgi might help.
24+
25+
26+
--
27+
**Pixels Camp 2017 CTF**

WebHacking/200-RegainSession/data/.keep

Whitespace-only changes.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
version: "2"
2+
services:
3+
redis:
4+
image: "redis"
5+
ports:
6+
- "6379"
7+
links:
8+
- uwsgi
9+
volumes:
10+
- ./data:/data
11+
command: redis-server --appendonly yes
12+
restart: always
13+
uwsgi:
14+
build:
15+
context: .
16+
dockerfile: ./docker/uwsgi/Dockerfile
17+
ports:
18+
- "30200:8000"
19+
volumes:
20+
- ./src:/code
21+
restart: always
22+
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
FROM python:3.6-jessie
2+
3+
ADD ./src/requirements.txt /
4+
5+
# Install build deps, then run `pip install`, then remove unneeded build deps all in a single step. Correct the path to your production requirements file, if needed.
6+
RUN set -ex \
7+
&& python3 -m venv /venv \
8+
&& /venv/bin/pip install -U pip \
9+
&& UWSGI_PROFILE=gevent /venv/bin/pip install -U uwsgi \
10+
&& /venv/bin/pip install --no-cache-dir -r /requirements.txt
11+
12+
# Copy your application code to the container (make sure you create a .dockerignore file if any large files or directories should be excluded)
13+
ADD ./src /code
14+
WORKDIR /code
15+
RUN ls /code
16+
17+
# uWSGI will listen on this port
18+
EXPOSE 8000
19+
20+
# uWSGI configuration (customize as needed):
21+
ENV UWSGI_VIRTUALENV=/venv UWSGI_HTTP=:8000 \
22+
UWSGI_MASTER=1 UWSGI_WORKERS=4 \
23+
UWSGI_WSGI_ENV_BEHAVIOR=holy UWSGI_VACUUM=1
24+
25+
# Start uWSGI
26+
CMD ["/venv/bin/uwsgi", "--gevent", "100", "--file", "app.py"]
27+
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
2+
from gevent import monkey; monkey.patch_all()
3+
4+
import bottle
5+
import json
6+
import jwt
7+
from bottle_redis import RedisPlugin
8+
from uuid import uuid4
9+
from random import randint
10+
11+
app = bottle.Bottle()
12+
FLAG = "flag{Y-ARHq29rhchpFJjyJyr}"
13+
14+
@app.get('/api/users/<username>')
15+
def callback(username, rdb):
16+
"Returns if an username exists"
17+
user = rdb.get("user:%s" % username)
18+
19+
if user is None:
20+
bottle.abort(404, "The user does not exist")
21+
user = json.loads(user)
22+
23+
auth_header = bottle.request.headers.get('Authorization')
24+
auth = False
25+
if auth_header is not None and auth_header.startswith("Bearer "):
26+
jwt_header = auth_header.split(" ")[1]
27+
try:
28+
user_jwt = jwt.decode(jwt_header, user.get('token'), algorithms=['HS256'])
29+
if user_jwt.get('username') == username and \
30+
user_jwt.get('timestamp', 0) > user.get('last_timestamp', 0):
31+
32+
user['last_timestamp'] = max(user.get('timestamp', 0), user_jwt.get('timestamp', 0))
33+
rdb.set("user:%s" % user.get('username'), json.dumps(user))
34+
35+
auth = True
36+
print(repr(user_jwt))
37+
except jwt.exceptions.DecodeError as err:
38+
print(repr(err))
39+
40+
if auth and user is not None:
41+
42+
if user.get('show_flag', False):
43+
user['locked'] = False
44+
rdb.set("user:%s" % user.get('username'), json.dumps(user))
45+
46+
return {
47+
"name": user.get('name'),
48+
"username": user.get('username'),
49+
"token": user.get('token')
50+
}
51+
else:
52+
return {
53+
'exists': user is not None
54+
}
55+
56+
@app.get('/api/usert/<token>')
57+
def callback(token, rdb):
58+
"Returns the username that belongs to the token"
59+
username = rdb.get("token:%s" % token)
60+
if username is None:
61+
bottle.abort(404, "The user does not exist.")
62+
return {
63+
'username': username.decode('utf-8')
64+
}
65+
66+
@app.post('/api/users')
67+
def callback(rdb):
68+
"Creates a new user"
69+
70+
data = bottle.request.json
71+
72+
fields = ["name", "username", "password"]
73+
missing_fields = []
74+
for field in fields:
75+
if field not in data or len(data.get(field)) <= 0:
76+
missing_fields.append(field)
77+
78+
if len(missing_fields) > 0:
79+
return {"success": False, "message": "All fields are required. Missing fields: %s" % ', '.join(missing_fields)}
80+
81+
user = rdb.get("user:%s" % data.get('username'))
82+
if user is not None:
83+
return {"success": False, "message": "This username already exists."}
84+
85+
token_generated = False
86+
while not token_generated:
87+
token = uuid4().hex
88+
rtoken = rdb.get("token:%s" % token)
89+
if rtoken is None:
90+
token_generated = True
91+
92+
rdb.set("user:%s" % data.get('username'), json.dumps({
93+
"name": data.get("name"),
94+
"username": data.get("username"),
95+
"password": data.get("password"),
96+
"token": token,
97+
"metric_count": 0,
98+
"locked": False,
99+
"show_flag": False,
100+
"last_timestamp": 0
101+
}))
102+
rdb.set("token:%s" % token, data.get("username"))
103+
104+
return {"success": True}
105+
106+
107+
@app.post('/api/authenticate')
108+
def callback(rdb):
109+
"Logins a user"
110+
111+
data = bottle.request.json
112+
if 'username' not in data or 'password' not in data:
113+
return {"success": False, "message": "Username and Password required"}
114+
115+
user = rdb.get("user:%s" % data.get('username'))
116+
117+
success = False
118+
if user is not None:
119+
user = json.loads(user)
120+
if user.get('password') == data.get('password'):
121+
success = True
122+
123+
if success:
124+
return {
125+
"success": True, "data": {
126+
"name": user.get('name'),
127+
"username": user.get('username'),
128+
"token": user.get('token')
129+
}
130+
}
131+
else:
132+
return {"success": False, "message": "Invalid username or password"}
133+
134+
@app.get('/api/metrics')
135+
def callback(rdb):
136+
137+
auth_header = bottle.request.headers.get('Authorization')
138+
139+
if auth_header is None or not auth_header.startswith("Bearer "):
140+
bottle.abort(403, "Forbidden")
141+
142+
jwt_header = auth_header.split(" ")[1]
143+
try:
144+
unvalidated_jwt = jwt.decode(jwt_header, verify=False)
145+
if unvalidated_jwt is None or 'username' not in unvalidated_jwt:
146+
bottle.abort(403, "Forbidden")
147+
148+
user = rdb.get("user:%s" % unvalidated_jwt.get('username'))
149+
if user is None:
150+
bottle.abort(403, "Forbidden")
151+
152+
user = json.loads(user)
153+
user_jwt = jwt.decode(jwt_header, user.get('token'), algorithms=['HS256'])
154+
155+
if user.get('last_timestamp', 0) >= user_jwt.get('timestamp', 0):
156+
bottle.abort(403, "Forbidden")
157+
158+
user['last_timestamp'] = max(user.get('timestamp', 0), user_jwt.get('timestamp', 0))
159+
user['metric_count'] = user.get('metric_count', 0) + 1
160+
if user['metric_count'] >= 3 and not user.get('show_flag', False):
161+
user['locked'] = True
162+
user['show_flag'] = True
163+
user['password'] = "9X%DuAHDj!!PjhQK%p^gPjSgG9"
164+
165+
rdb.set("user:%s" % user.get('username'), json.dumps(user))
166+
167+
print(repr(user_jwt))
168+
print(repr(user))
169+
except jwt.exceptions.DecodeError as e:
170+
bottle.abort(403, "Forbidden")
171+
172+
if user.get('locked', False):
173+
return {"success": False, "message": "Detected an unknown access location. For your security we changed your password and logged you out."}
174+
175+
metrics = {
176+
'metrics': [
177+
{'name': 'Current Users', 'value': randint(52, 57)},
178+
{'name': 'Max Users', 'value': 133},
179+
{'name': 'Current Ping', 'value': randint(52, 2333)},
180+
{'name': 'Max Ping', 'value': 80082}
181+
]
182+
}
183+
184+
if user.get('show_flag', False):
185+
metrics['metrics'].append({'name': 'Flag', 'value': 'flag{Y-ARHq29rhchpFJjyJyr}'})
186+
187+
return metrics
188+
189+
@app.get('/')
190+
def callback():
191+
response = bottle.static_file("index.html", "./static")
192+
response.set_header("Cache-Control", "public, max-age=1")
193+
return response
194+
195+
@app.get('/<path:path>')
196+
def callback(path):
197+
response = bottle.static_file(path, "./static")
198+
response.set_header("Cache-Control", "public, max-age=1")
199+
return response
200+
201+
202+
if __name__ == '__main__':
203+
app.install(RedisPlugin(host="localhost"))
204+
app.run(
205+
host='localhost',
206+
port=8080,
207+
debug=True,
208+
reloader=True
209+
)
210+
else:
211+
app.install(RedisPlugin(host="redis"))
212+
application = app
213+
214+
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
bottle==0.12.13
2+
bottle-redis==0.2.3
3+
gevent==1.2.2
4+
greenlet==0.4.12
5+
PyJWT==1.5.3
6+
redis==2.10.6
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
a {
2+
cursor: pointer;
3+
}

0 commit comments

Comments
 (0)