Add Team 14 CSRF team project materials (정보보호, Korea Univ.)
This commit is contained in:
1 parent
f53f19e2ab
commit
4b914f8c4a
22 files changed
+405
No files matched your search
@@ -0,0 +1,5 @@
|
||||
team-project-csrf/**/*.pptx filter=lfs diff=lfs merge=lfs -text
|
||||
team-project-csrf/**/*.docx filter=lfs diff=lfs merge=lfs -text
|
||||
team-project-csrf/**/*.pdf filter=lfs diff=lfs merge=lfs -text
|
||||
team-project-csrf/**/*.zip filter=lfs diff=lfs merge=lfs -text
|
||||
team-project-csrf/**/*.png filter=lfs diff=lfs merge=lfs -text
|
||||
@@ -0,0 +1,26 @@
|
||||
# team-project-csrf
|
||||
|
||||
Korea University · 정보보호 (Information Security) — **Team 14** team project, topic: **Cross-Site Request Forgery (CSRF)**.
|
||||
|
||||
Archive of project materials, copied as-is from the original files (no content edited). Original filenames used an em dash (—); they are renamed here to ASCII-safe names.
|
||||
|
||||
> **Deadline:** Final Report due **2026-10-08 23:59 KST**.
|
||||
|
||||
## File index
|
||||
|
||||
Dates are the original files' last-modified times (KST).
|
||||
|
||||
| Path | Original filename | Date |
|
||||
|---|---|---|
|
||||
| `report/Team14-CSRF-Presentation-Materials-Report.docx` | `Team 14 — CSRF Presentation Materials (Report).docx` | 2026-10-07 17:25 |
|
||||
| `report/Cross-Site-Request-Forgery_Team14.pdf` | `Cross-Site Request Forgery_Team14.pdf` | 2026-10-07 09:58 |
|
||||
| `slides/Team14-CSRF-Revised-Slides.pptx` | `Team 14 — CSRF (Revised Slides).pptx` (current slides) | 2026-10-07 17:25 |
|
||||
| `slides/archive/Cross-Site-Request-Forgery_Team14_original-2026-09-30.pptx` | `Cross-Site Request Forgery_Team14.pptx` (original slides) | 2026-09-30 23:23 |
|
||||
| `figures/fig1..fig9_annotated.png`, `figures/annotate.py` | contents of `figures_annotated.zip` | 2026-10-07 17:24 (zip) |
|
||||
| `figures/fig9_experiment_c.png` | `fig9_experiment_c.png` | 2026-10-07 10:58 |
|
||||
| `experiments/experiment_c/` (`csrf_gate.py`, `replay.sh`, `render_fig9.py`, `client.log`, `server.log`) | contents of `experiment_c.zip` | 2026-10-07 10:57 (zip) |
|
||||
|
||||
## Notes
|
||||
|
||||
- Binary files (`*.pptx *.docx *.pdf *.zip *.png`) are stored with Git LFS.
|
||||
- 本仓库仅作归档 / archive only.
|
||||
@@ -0,0 +1,11 @@
|
||||
client: replay.sh
|
||||
----------------------------------------------------------------------------------------
|
||||
C0 control: genuine form, same-origin, token present -> wiener.new@ -> HTTP 200
|
||||
A1 §3.1 GET /change-email?email=attacker, token dropped, cross-site -> HTTP 405
|
||||
A2 §3.2B GET …&_method=POST (method override), cross-site -> HTTP 405
|
||||
A3 §3.2A POST, token absent, cross-site form -> HTTP 403
|
||||
A4 POST from evil.example.com (same-site sibling), token absent -> HTTP 403
|
||||
A5 POST with allowed Origin, token absent (layer 3 alone must hold) -> HTTP 403
|
||||
A6 POST with allowed Origin, token from another session -> HTTP 403
|
||||
----------------------------------------------------------------------------------------
|
||||
final state: current email: [email protected]
|
||||
@@ -0,0 +1,87 @@
|
||||
# csrf_gate.py — Experiment C: one gate, every unsafe method, nothing routes around it
|
||||
# Run: python3 csrf_gate.py (HTTPS on 127.0.0.1:8443, self-signed cert.pem/key.pem)
|
||||
# Then: bash replay.sh (replays the attack shapes of §3.1 / §3.2 and a control)
|
||||
import hmac, logging, secrets, sys
|
||||
from flask import Flask, abort, request, session, render_template_string
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = secrets.token_bytes(32) # per-process for the lab; persist in production
|
||||
app.config.update(
|
||||
SESSION_COOKIE_SAMESITE="Lax", # layer 1 — enforced by the browser (experiment B)
|
||||
SESSION_COOKIE_SECURE=True,
|
||||
SESSION_COOKIE_HTTPONLY=True,
|
||||
)
|
||||
SAFE = {"GET", "HEAD", "OPTIONS"}
|
||||
ALLOWED_ORIGINS = {"https://app.example.com"} # exact origins, never bare domains
|
||||
|
||||
logging.basicConfig(stream=sys.stdout, level=logging.INFO, format="%(message)s")
|
||||
log = logging.getLogger("gate")
|
||||
|
||||
|
||||
def reject(status, layer, reason):
|
||||
log.info(f"[GATE] REJECT {status} {request.method:<4} {request.full_path.rstrip('?'):<48} layer={layer:<2} {reason}")
|
||||
abort(status)
|
||||
|
||||
|
||||
@app.before_request
|
||||
def csrf_gate():
|
||||
if request.method in SAFE: # safe methods never change state (routing enforces it)
|
||||
return
|
||||
# layer 2a — browser-asserted provenance; page script cannot set this header
|
||||
if request.headers.get("Sec-Fetch-Site") == "cross-site":
|
||||
reject(403, "2a", "Sec-Fetch-Site: cross-site")
|
||||
# layer 2b — exact-origin allow-list; catches sibling subdomains that are same-site
|
||||
origin = request.headers.get("Origin")
|
||||
if origin is None or origin not in ALLOWED_ORIGINS:
|
||||
reject(403, "2b", f"Origin {origin} not in allow-list")
|
||||
# layer 3 — session-bound synchronizer token, constant-time compare
|
||||
expected = session.get("csrf", "")
|
||||
supplied = request.form.get("csrf") or request.headers.get("X-CSRF-Token", "")
|
||||
if not expected or not hmac.compare_digest(expected.encode(), supplied.encode()):
|
||||
reject(403, "3", "token missing or mismatched")
|
||||
|
||||
|
||||
def csrf_token(): # called by the template that renders the form
|
||||
return session.setdefault("csrf", secrets.token_urlsafe(32))
|
||||
|
||||
|
||||
@app.errorhandler(405)
|
||||
def method_not_allowed(_e): # layer 0 — the route itself refuses GET
|
||||
log.info(f"[GATE] REJECT 405 {request.method:<4} {request.full_path.rstrip('?'):<48} layer=0 method not allowed on state-changing route")
|
||||
return "Method Not Allowed\n", 405
|
||||
|
||||
|
||||
# ---- scaffolding for the experiment (login, account page, state) — not part of the gate ----
|
||||
USERS = {"wiener": {"email": "[email protected]"}}
|
||||
|
||||
|
||||
@app.get("/login")
|
||||
def login():
|
||||
session["user"] = "wiener"
|
||||
csrf_token()
|
||||
return "logged in as wiener\n"
|
||||
|
||||
|
||||
@app.get("/my-account")
|
||||
def my_account():
|
||||
if "user" not in session:
|
||||
abort(401)
|
||||
return render_template_string(
|
||||
'<form method="POST" action="/change-email">'
|
||||
'<input type="hidden" name="csrf" value="{{ t }}">'
|
||||
'<input name="email"><button>Update email</button></form>\n'
|
||||
'current email: {{ e }}\n',
|
||||
t=csrf_token(), e=USERS["wiener"]["email"])
|
||||
|
||||
|
||||
@app.post("/change-email") # layer 0: POST only → GET receives 405 from Flask
|
||||
def change_email():
|
||||
if "user" not in session:
|
||||
abort(401)
|
||||
USERS[session["user"]]["email"] = request.form["email"]
|
||||
log.info(f"[APP ] ACCEPT 200 POST /change-email{'':<36} layers 0,2a,2b,3 passed email={request.form['email']}")
|
||||
return f"email changed to {request.form['email']}\n"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="127.0.0.1", port=8443, ssl_context=("cert.pem", "key.pem"))
|
||||
@@ -0,0 +1,48 @@
|
||||
# render_fig9.py — turn client.log + server.log into one terminal-style PNG (Fig. 9)
|
||||
import re
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
MONO = "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf"
|
||||
BOLD = "/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf"
|
||||
font = ImageFont.truetype(MONO, 22)
|
||||
bold = ImageFont.truetype(BOLD, 22)
|
||||
|
||||
client = open("client.log", encoding="utf-8").read().rstrip().splitlines()
|
||||
server = [l for l in open("server.log", encoding="utf-8").read().splitlines()
|
||||
if l.startswith(("[GATE]", "[APP ]", " * Running"))]
|
||||
|
||||
BG, FG, DIM, OK, BAD, HEAD = (24, 26, 36), (220, 223, 228), (130, 135, 150), (120, 220, 140), (255, 120, 120), (140, 180, 255)
|
||||
|
||||
def color_for(line):
|
||||
if "ACCEPT" in line or "HTTP 200" in line:
|
||||
return OK
|
||||
if "REJECT" in line or "HTTP 4" in line:
|
||||
return BAD
|
||||
if line.startswith(("client:", "server:")):
|
||||
return HEAD
|
||||
if set(line) <= {"-"} or line.startswith(" *"):
|
||||
return DIM
|
||||
return FG
|
||||
|
||||
blocks = [("client: replay.sh (victim's browser, cookie attached in every request)", client[1:]),
|
||||
("server: csrf_gate.py (gate decisions)", server)]
|
||||
|
||||
lines = []
|
||||
for title, body in blocks:
|
||||
lines.append((title, HEAD, bold))
|
||||
for l in body:
|
||||
lines.append((l, color_for(l), bold if ("ACCEPT" in l or "REJECT" in l or "HTTP" in l) else font))
|
||||
lines.append(("", FG, font))
|
||||
|
||||
LH, PAD = 32, 36
|
||||
width = max(int(d.textlength(t, font=f)) for t, _, f in lines for d in [ImageDraw.Draw(Image.new("RGB", (1, 1)))]) + 2 * PAD
|
||||
height = LH * len(lines) + 2 * PAD + 40
|
||||
img = Image.new("RGB", (width, height), BG)
|
||||
d = ImageDraw.Draw(img)
|
||||
d.text((PAD, 18), "Experiment C — layered CSRF gate vs. the attack shapes of §3.1 / §3.2 (Flask 3.1, curl, 2026-10-07)", fill=DIM, font=bold)
|
||||
y = PAD + 40
|
||||
for t, c, f in lines:
|
||||
d.text((PAD, y), t, fill=c, font=f)
|
||||
y += LH
|
||||
img.save("fig9_experiment_c.png")
|
||||
print(img.size)
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
# replay.sh — Experiment C: replay the attack shapes of §3.1 / §3.2 against csrf_gate.py
|
||||
# Each request carries the headers a browser would send for that situation. The session
|
||||
# cookie is attached in every case, deliberately: the browser-side SameSite layer is
|
||||
# exercised in experiment B; here we test whether the SERVER holds on its own.
|
||||
set -u
|
||||
B=https://127.0.0.1:8443
|
||||
J=$(mktemp) # cookie jar = the victim's logged-in browser
|
||||
C="curl -sk -b $J -c $J -o /dev/null -w %{http_code}"
|
||||
|
||||
curl -sk -c "$J" "$B/login" >/dev/null # victim logs in
|
||||
TOKEN=$(curl -sk -b "$J" "$B/my-account" | grep -o 'name="csrf" value="[^"]*"' | cut -d'"' -f4)
|
||||
|
||||
run() { printf '%-3s %-64s -> HTTP %s\n' "$1" "$2" "$3"; }
|
||||
|
||||
echo "client: replay.sh"
|
||||
echo "----------------------------------------------------------------------------------------"
|
||||
|
||||
run C0 "control: genuine form, same-origin, token present -> wiener.new@" \
|
||||
"$($C -X POST "$B/change-email" -H "Origin: https://app.example.com" -H "Sec-Fetch-Site: same-origin" -H "Sec-Fetch-Mode: navigate" --data "csrf=$TOKEN&[email protected]")"
|
||||
|
||||
run A1 "§3.1 GET /change-email?email=attacker, token dropped, cross-site" \
|
||||
"$($C "$B/[email protected]" -H "Sec-Fetch-Site: cross-site" -H "Sec-Fetch-Mode: navigate")"
|
||||
|
||||
run A2 "§3.2B GET …&_method=POST (method override), cross-site" \
|
||||
"$($C "$B/[email protected]&_method=POST" -H "Sec-Fetch-Site: cross-site" -H "Sec-Fetch-Mode: navigate")"
|
||||
|
||||
run A3 "§3.2A POST, token absent, cross-site form" \
|
||||
"$($C -X POST "$B/change-email" -H "Origin: https://evil.net" -H "Sec-Fetch-Site: cross-site" -H "Sec-Fetch-Mode: navigate" --data "[email protected]")"
|
||||
|
||||
run A4 "POST from evil.example.com (same-site sibling), token absent" \
|
||||
"$($C -X POST "$B/change-email" -H "Origin: https://evil.example.com" -H "Sec-Fetch-Site: same-site" -H "Sec-Fetch-Mode: navigate" --data "[email protected]")"
|
||||
|
||||
run A5 "POST with allowed Origin, token absent (layer 3 alone must hold)" \
|
||||
"$($C -X POST "$B/change-email" -H "Origin: https://app.example.com" -H "Sec-Fetch-Site: same-origin" -H "Sec-Fetch-Mode: navigate" --data "[email protected]")"
|
||||
|
||||
run A6 "POST with allowed Origin, token from another session" \
|
||||
"$($C -X POST "$B/change-email" -H "Origin: https://app.example.com" -H "Sec-Fetch-Site: same-origin" -H "Sec-Fetch-Mode: navigate" --data "csrf=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&[email protected]")"
|
||||
|
||||
echo "----------------------------------------------------------------------------------------"
|
||||
printf 'final state: %s\n' "$(curl -sk -b "$J" "$B/my-account" | grep -o 'current email: .*')"
|
||||
rm -f "$J"
|
||||
@@ -0,0 +1,19 @@
|
||||
* Serving Flask app 'csrf_gate'
|
||||
* Running on https://127.0.0.1:8443
|
||||
127.0.0.1 - - [07/Oct/2026 10:16:42] "GET /login HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [07/Oct/2026 10:16:42] "GET /my-account HTTP/1.1" 200 -
|
||||
[APP ] ACCEPT 200 POST /change-email layers 0,2a,2b,3 passed [email protected]
|
||||
127.0.0.1 - - [07/Oct/2026 10:16:42] "POST /change-email HTTP/1.1" 200 -
|
||||
[GATE] REJECT 405 GET /[email protected] layer=0 method not allowed on state-changing route
|
||||
127.0.0.1 - - [07/Oct/2026 10:16:42] "GET /[email protected] HTTP/1.1" 405 -
|
||||
[GATE] REJECT 405 GET /[email protected]&_method=POST layer=0 method not allowed on state-changing route
|
||||
127.0.0.1 - - [07/Oct/2026 10:16:42] "GET /[email protected]&_method=POST HTTP/1.1" 405 -
|
||||
[GATE] REJECT 403 POST /change-email layer=2a Sec-Fetch-Site: cross-site
|
||||
127.0.0.1 - - [07/Oct/2026 10:16:42] "POST /change-email HTTP/1.1" 403 -
|
||||
[GATE] REJECT 403 POST /change-email layer=2b Origin https://evil.example.com not in allow-list
|
||||
127.0.0.1 - - [07/Oct/2026 10:16:42] "POST /change-email HTTP/1.1" 403 -
|
||||
[GATE] REJECT 403 POST /change-email layer=3 token missing or mismatched
|
||||
127.0.0.1 - - [07/Oct/2026 10:16:42] "POST /change-email HTTP/1.1" 403 -
|
||||
[GATE] REJECT 403 POST /change-email layer=3 token missing or mismatched
|
||||
127.0.0.1 - - [07/Oct/2026 10:16:42] "POST /change-email HTTP/1.1" 403 -
|
||||
127.0.0.1 - - [07/Oct/2026 10:16:42] "GET /my-account HTTP/1.1" 200 -
|
||||
@@ -0,0 +1,125 @@
|
||||
# annotate.py — draw numbered red boxes + dark callout labels on the lab screenshots.
|
||||
# Coordinates below are in a 2000x1200 reference space; S scales them to the real 2880x1728.
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
S = 2880 / 2000
|
||||
RED = (229, 50, 45)
|
||||
NAVY = (27, 29, 58)
|
||||
WHITE = (255, 255, 255)
|
||||
FONT = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 38)
|
||||
BADGE = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 34)
|
||||
STROKE = 6
|
||||
|
||||
|
||||
def sc(v):
|
||||
return int(round(v * S))
|
||||
|
||||
|
||||
def annotate(src, dst, items):
|
||||
im = Image.open(src).convert("RGBA")
|
||||
ov = Image.new("RGBA", im.size, (0, 0, 0, 0))
|
||||
d = ImageDraw.Draw(ov)
|
||||
for n, it in enumerate(items, 1):
|
||||
x1, y1, x2, y2 = map(sc, it["box"])
|
||||
d.rounded_rectangle((x1, y1, x2, y2), radius=10, outline=RED, width=STROKE)
|
||||
# badge at the box's top-left corner
|
||||
bx, by, r = x1, y1, 26
|
||||
d.ellipse((bx - r, by - r, bx + r, by + r), fill=RED)
|
||||
tw = d.textlength(str(n), font=BADGE)
|
||||
d.text((bx - tw / 2, by - 24), str(n), fill=WHITE, font=BADGE)
|
||||
# callout label
|
||||
lx, ly = map(sc, it["label_at"])
|
||||
text = f"{n} {it['label']}"
|
||||
tw = d.textlength(text, font=FONT)
|
||||
pad = 16
|
||||
lbox = (lx, ly, lx + tw + 2 * pad, ly + 38 + 2 * pad)
|
||||
# leader line from label to the nearest box edge
|
||||
cx = min(max((lbox[0] + lbox[2]) / 2, x1), x2)
|
||||
cy = y1 if lbox[3] < y1 else (y2 if lbox[1] > y2 else (y1 + y2) / 2)
|
||||
ax = lbox[0] if cx < lbox[0] else (lbox[2] if cx > lbox[2] else cx)
|
||||
ay = lbox[3] if cy > lbox[3] else (lbox[1] if cy < lbox[1] else (lbox[1] + lbox[3]) / 2)
|
||||
if it.get("leader", True):
|
||||
d.line((ax, ay, cx, cy), fill=RED, width=5)
|
||||
d.rounded_rectangle(lbox, radius=12, fill=NAVY + (235,))
|
||||
d.text((lx + pad, ly + pad - 2), text, fill=WHITE, font=FONT)
|
||||
out = Image.alpha_composite(im, ov).convert("RGB")
|
||||
out.save(dst, optimize=True)
|
||||
print(dst, out.size)
|
||||
|
||||
|
||||
annotate("fig1_original_post.png", "fig1_annotated.png", [
|
||||
{"box": (576, 594, 1424, 619), "label": "Genuine POST /change-email -> 302 (email changed)",
|
||||
"label_at": (640, 300)},
|
||||
{"box": (14, 709, 562, 733), "label": "Session cookie: attached by the browser automatically",
|
||||
"label_at": (600, 900)},
|
||||
{"box": (1436, 644, 1780, 690), "label": "csrf token in the form body",
|
||||
"label_at": (1445, 720)},
|
||||
])
|
||||
|
||||
annotate("fig2_wrong_token_400.png", "fig2_annotated.png", [
|
||||
{"box": (8, 1060, 566, 1092), "label": "Same POST resent with the csrf value altered",
|
||||
"label_at": (14, 1112)},
|
||||
{"box": (576, 844, 1424, 869), "label": "Server rejects it: 400",
|
||||
"label_at": (640, 960)},
|
||||
{"box": (1436, 678, 1780, 702), "label": "\"Invalid CSRF token\": the check works for POST",
|
||||
"label_at": (600, 1040)},
|
||||
])
|
||||
|
||||
annotate("fig3_get_no_token_302.png", "fig3_annotated.png", [
|
||||
{"box": (20, 949, 1424, 974), "label": "GET /change-email?email=... with NO token -> 302 accepted",
|
||||
"label_at": (300, 898)},
|
||||
{"box": (1436, 676, 1780, 836), "label": "302 -> Location: /my-account",
|
||||
"label_at": (1445, 850)},
|
||||
{"box": (398, 262, 664, 294), "label": "Email changed - the token was never checked on GET",
|
||||
"label_at": (690, 262)},
|
||||
{"box": (20, 699, 1424, 723), "label": "Control: same GET on a lab WITHOUT the flaw -> 405",
|
||||
"label_at": (300, 648)},
|
||||
])
|
||||
|
||||
annotate("fig4_solved.png", "fig4_annotated.png", [
|
||||
{"box": (398, 110, 802, 157), "label": "Lab solved by the delivered page",
|
||||
"label_at": (840, 112)},
|
||||
{"box": (406, 858, 1292, 982), "label": "A form with no method attribute (= GET) and no csrf field",
|
||||
"label_at": (414, 1000)},
|
||||
])
|
||||
|
||||
annotate("fig5_no_token_302.png", "fig5_annotated.png", [
|
||||
{"box": (8, 972, 566, 1000), "label": "Resent POST: csrf parameter DELETED, not altered",
|
||||
"label_at": (14, 1030)},
|
||||
{"box": (576, 593, 1424, 617), "label": "Accepted: 302 with no csrf parameter at all",
|
||||
"label_at": (640, 720)},
|
||||
{"box": (398, 246, 662, 278), "label": "Email changed with no token at all",
|
||||
"label_at": (690, 244)},
|
||||
])
|
||||
|
||||
annotate("fig6_labA_solved.png", "fig6_annotated.png", [
|
||||
{"box": (398, 248, 800, 286), "label": "Victim's email changed by a POST form carrying only email",
|
||||
"label_at": (406, 192)},
|
||||
{"box": (1398, 108, 1598, 150), "label": "Lab status: Solved",
|
||||
"label_at": (1640, 170)},
|
||||
])
|
||||
|
||||
annotate("fig7_plain_get_405.png", "fig7_annotated.png", [
|
||||
{"box": (20, 367, 1800, 392), "label": "Plain GET /change-email?email=... (no _method) -> 405",
|
||||
"label_at": (300, 460)},
|
||||
{"box": (8, 150, 220, 176), "label": "Method Not Allowed: the route refuses GET",
|
||||
"label_at": (260, 148)},
|
||||
])
|
||||
|
||||
annotate("fig8_method_override_302.png", "fig8_annotated.png", [
|
||||
{"box": (330, 54, 1330, 80), "label": "Same GET with &_method=POST appended",
|
||||
"label_at": (660, 190)},
|
||||
{"box": (20, 417, 1800, 442), "label": "Accepted: 302 - the override turns the GET into a POST",
|
||||
"label_at": (300, 700)},
|
||||
{"box": (398, 238, 662, 266), "label": "Email changed",
|
||||
"label_at": (690, 236)},
|
||||
{"box": (20, 367, 1800, 392), "label": "Control: no _method -> 405 (Fig. 7)",
|
||||
"label_at": (1300, 700)},
|
||||
])
|
||||
|
||||
annotate("fig9_labB_solved.png", "fig9_annotated.png", [
|
||||
{"box": (398, 104, 800, 143), "label": "Lab solved",
|
||||
"label_at": (830, 103)},
|
||||
{"box": (406, 850, 1350, 932), "label": "Delivered page: a top-level navigation to the GET URL carrying _method=POST",
|
||||
"label_at": (414, 955)},
|
||||
])
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Reference in new issue
Block a user