//Antakshari
Flag: nite{Diehard_1891771341083729}
>Challenge Summary
We receive two NumPy arrays:
handout/latent_vectors.npy– 201 embedding vectors, 64 dims eachhandout/partial_edges.npy– four known edges connecting 6 actor nodes (cast members)
The hosted app asks: “Enter the Actor Node Sequence in Descending Order.” Submitting a correct 6-node sequence of actors returns the flag via /api/verify.
>Local Analysis
- Inspect shapes/values:
python3 - <<'PY'
import numpy as np
lv=np.load('handout/latent_vectors.npy')
edges=np.load('handout/partial_edges.npy')
print(lv.shape, edges.shape)
print(edges)
PY- 201 nodes, 64-dim vectors; 4 edges hint at two-movie pairs.
- Hypothesis: embeddings encode a bipartite movie–actor graph; similarities (dot products) are high between co-cast and lower otherwise.
- Split nodes into two clusters by embedding norm (high norm ≈ movies, low norm ≈ actors). KMeans on norms cleanly gives two groups (86 vs 115 nodes). Higher-norm cluster is treated as movies.
- Build similarity matrix
M = lv @ lv.Tand, for each movie, rank actor nodes by dot product. Top scores correlate with the partial edges (e.g., movie 149 ↔ actors 18,158,...; movie 12 ↔ 124,...; movie 45 ↔ 99,...).
>Automated Search for the Correct Cast
The web endpoint expects exactly six actor IDs in descending order. I automated trying every movie node: take its top-6 most similar actors, sort them descending, submit, and stop when the response is not “Incorrect”.
Script used (works locally and remotely—change url if needed):
import numpy as np, requests, json
from sklearn.cluster import KMeans
lv = np.load('handout/latent_vectors.npy')
norms = np.linalg.norm(lv, axis=1)
labels = KMeans(n_clusters=2, random_state=0).fit(norms.reshape(-1, 1)).labels_
movie_label = 1 if norms[labels==1].mean() > norms[labels==0].mean() else 0
movies = np.where(labels == movie_label)[0]
actors = np.where(labels != movie_label)[0]
M = lv @ lv.T
url = '<https://antakshari1.chall.nitectf25.live/api/verify>'
headers = {'Content-Type': 'application/json'}
for movie in movies:
sims = M[movie, actors]
order = np.argsort(-sims)
top6 = actors[order[:6]]
seq = ','.join(str(x) for x in sorted(top6, reverse=True))
r = requests.post(url, headers=headers, data=json.dumps({'node_sequence': seq}), timeout=10)
res = r.json().get('result', '')
print(f"movie {movie} -> {seq} -> {res}")
if 'Incorrect' not in res:
break
When run, the first success appears at movie node 3:
movie 3 -> 189,177,134,108,37,29 -> nite{Diehard_1891771341083729}
So the required actor node sequence is 189,177,134,108,37,29 (already in descending order), yielding the flag.
>Remote Exploitation
No auth or CSRF—just POST JSON to /api/verify:
curl -s -X POST <https://antakshari1.chall.nitectf25.live/api/verify> \\
-H 'Content-Type: application/json' \\
-d '{"node_sequence":"189,177,134,108,37,29"}'
Returns the flag string above.
>Full Solver (reproducible)
Below is the full solver.py script used during the contest; it is available at the project root. It implements the two-cluster norm split, selects top‑6 actor candidates per movie by dot product, sorts them in descending order (as required), and optionally queries the remote /api/verify endpoint.
#!/usr/bin/env python3
"""Solver for Antakshari challenge
Usage:
python3 solver.py --local-only # print candidate top-6 actor sequences
python3 solver.py --url https://... # test sequences against remote /api/verify
"""
import argparse
import json
import numpy as np
from sklearn.cluster import KMeans
def predict_top6(lv):
norms = np.linalg.norm(lv, axis=1)
labels = KMeans(n_clusters=2, random_state=0).fit(norms.reshape(-1, 1)).labels_
# choose movie label as the cluster with higher mean norm
movie_label = 1 if norms[labels == 1].mean() > norms[labels == 0].mean() else 0
movies = np.where(labels == movie_label)[0]
actors = np.where(labels != movie_label)[0]
M = lv @ lv.T
candidates = []
for movie in movies:
sims = M[movie, actors]
order = np.argsort(-sims)
top6 = actors[order[:6]]
seq_sorted_desc = ','.join(str(x) for x in sorted(top6, reverse=True))
candidates.append((movie, seq_sorted_desc, top6, sims[order[:6]]))
return candidates
def query_remote(url, seq):
import requests
headers = {'Content-Type': 'application/json'}
data = {'node_sequence': seq}
r = requests.post(url, headers=headers, data=json.dumps(data), timeout=10)
return r.status_code, r.text
def main():
p = argparse.ArgumentParser()
p.add_argument('--url', help='API verify URL (optional)')
p.add_argument('--local-only', action='store_true', help='Do not query remote; just print candidates')
p.add_argument('--vectors', default='handout/latent_vectors.npy', help='Path to latent_vectors.npy')
args = p.parse_args()
lv = np.load(args.vectors)
cands = predict_top6(lv)
# sort candidates by movie id for deterministic output
cands = sorted(cands, key=lambda x: x[0])
for movie, seq, top6, sims in cands:
print(f'Movie {movie}: seq={seq}')
if args.url and not args.local_only:
code, text = query_remote(args.url, seq)
print(' remote:', code, text)
if 'nite{' in text:
print('\\nFLAG FOUND:', text)
return
# Also attempt a focused heuristic: try top candidate per movie by similarity sum
# (already chosen above) - print top result explicitly
if not args.url or args.local_only:
print('\\nNo remote URL provided or local-only mode. Candidates are shown above.')
if __name__ == '__main__':
main()