Skip to main content

The PSG hostel WiFi login script, and the password I committed to GitHub at nineteen

Share:XLinkedInHN
Cover for The PSG hostel WiFi login script, and the password I committed to GitHub at nineteen

The problem, at nineteen

Third year at PSG Tech, hostel block, one captive portal in front of the floor WiFi. The session timed out every fifteen minutes. You would be mid-download or on a call, and the portal would silently drop you, and the only way back was to open a browser, wait for the redirect, tick a checkbox, type a username, type a password, and hit submit.

I did this once. I did it twice. I did it about forty times in one week and wrote a Python script.

The script lives at kaushiksaravanan/WiFi-Login-Automation. It is 110 lines of Selenium. It watches the interface, notices when the connection has dropped, opens the portal in a hidden Chrome, keyboard-hops through the terms and conditions checkbox, submits the form, and goes back to sleep. It also, in the same commit, cheerfully published a set of credentials to the public internet. I will get to that.

Reverse-engineering the portal

The portal answers at http://172.17.0.1:2280/cportal/ip/user_login.php?url=http://www.gstatic.com/generate_204. That IP and path is the visible surface of a Cyberoam or Sophos XG firewall. I never confirmed which one. What mattered was the URL was stable across the whole hostel network.

The gstatic.com/generate_204 endpoint returns an empty response with HTTP 204. Operating systems ping it on every network change. If they get a 204, the internet is real. If they get anything else, they know a portal is intercepting, and they open that page for the user. Building the same URL into the script meant it always talked to the same login page the OS would have shown.

The login form was inside an iframe named login_win. The username field had id usrname. The submit button had id update_btn. The password field had no useful attribute I could find, so I selected it by absolute XPath, which is exactly as fragile as it sounds. It worked because the portal had not changed in years.

The checkbox that refused to be clicked

Every captive portal has a terms and conditions checkbox. This one had a checkbox that, when clicked with Selenium, threw elementClickIntercepted. Some overlay or transparent div was sitting on top of it. I spent maybe an hour trying to execute_script("arguments[0].click()") past it before giving up and using the keyboard.

ActionChains(driver).send_keys(Keys.TAB * 2, Keys.SPACE).perform()

Tab twice from the username field, hit space, done. The keyboard event went through the compositor and the overlay never got a chance to intercept it. The mouse could not see the checkbox because something was in the way. The keyboard did not care about "in the way", because focus and hit-testing are two different problems in a browser.

Detecting the drop with netsh

disConnected() shells out to netsh wlan show interfaces, greps for the SSID line, and parses it by string offset. The README instructs the user to name their WiFi profile PSG BH (Block name - Room no), and the script confirms the SSID starts with PSG before doing anything.

Once it has confirmed the SSID, it fires a requests.get('https://github.com/') as a reachability probe. If GitHub answers, the session is still alive. If it does not, the portal has kicked us and we need to log in again.

Nineteen-year-old me thought this was clean. Twenty-six-year-old me thinks parsing netsh output by string offset is the sort of thing that works exactly once, on exactly one Windows locale, until an update shifts the offsets by two characters. It never occurred to me that Windows has a real API for this. It also never occurred to me that Python could talk directly to the portal without a browser at all, because the login flow was a plain HTML form POST.

The script, as a flow

flowchart TD
    A[netsh wlan show interfaces] --> B{SSID starts with PSG?}
    B -- no --> Z[sleep and retry]
    B -- yes --> C[open portal URL in Chrome]
    C --> D[switch into iframe login_win]
    D --> E[fill #usrname and password]
    E --> F[TAB TAB SPACE on T and C checkbox]
    F --> G[click #update_btn]
    G --> H[requests.get github.com]
    H -- 200 OK --> Z
    H -- fail --> C

That is the whole thing. About a dozen states, one iframe, one keyboard trick, one HTTP probe. It ran in the background of my hostel PC every night for the rest of that year.

The credentials I committed to GitHub

Now the part I owe an honest post about.

Open main.py. Scroll to line 108. There is a call that reads, roughly, login("20I224", "20I224"). Both arguments are the same string. That string is shaped like a PSG Tech roll number: batch 2020, department code I, roll 224. The password is the roll number typed a second time.

The repo is public. Every version under Documentation/ (v1 through v4) carries the same pair, unredacted. About sixty of the sixty-four commits on the repo are README touch-ups, so I had many chances to notice and did not.

I want to be careful here. I do not want to confirm whose roll number that is. Roll numbers at Indian engineering colleges are treated like partial IDs, and I already made the mistake of publishing one; I am not going to make it worse by attaching a name in a blog post that will outlive the repo. Treat the string as a roll-number-shaped string. The point stands either way, which is that a real credential pair was sitting in a public GitHub repo, indexed, cached, and mirrored, for years.

Anything that was ever a real credential in that repo has long since been rotated. Nothing in the current repo grants access to anything. That is not because I noticed in time. It is because captive portals get replaced, batches graduate, and PSG's IT rotates roll numbers out of their radius directory. Time did the cleanup I should have done.

If you are a student reading this and you have a main.py somewhere with a real password in it, please:

  • Move creds to environment variables, or a .env file that is in .gitignore from the first commit.
  • Install git secrets or trufflehog as a pre-commit hook. Both scan for high-entropy strings and known credential shapes.
  • If you have already pushed a secret, git filter-repo --replace-text will rewrite history. GitHub's docs on removing sensitive data are worth reading before you try.
  • Rotate the credential even if you rewrote history. Assume it was scraped the moment it hit the public repo. Anyone who cloned in the window between push and rewrite still has it.

What I would do differently now

Not Selenium. The whole login flow was a form POST to a known URL. requests.Session() with a couple of hidden form fields would have replaced sixty lines of ChromeDriver management, webdriver_manager, and the auto-installer commit that came later.

Not netsh output parsing. Windows has WlanQueryInterface. Python has pywifi. macOS has networksetup. On any modern OS the "am I connected to a captive portal" question is a real API call.

Not pandas. It is listed in requirements.txt and never actually imported by main.py. It shipped alongside Selenium 4.2.0, which pins us to an ancient version because Selenium 4.3 removed the find_element_by_* methods the script depends on. The whole dependency tree could be one line: requests.

Not hardcode credentials. This one I really should have known at nineteen. Everyone tells you. Nobody listens until they see their own password on GitHub search.

The script is still on my profile because leaving it up, with this post pointing at it, feels more useful than quietly deleting it. Every student who writes their first automation script writes one of these. The value is in seeing what someone else did wrong and going, oh, right, I should not do that.

Cite as: Saravanan, K. (2026). The PSG hostel WiFi login script, and the password I committed to GitHub at nineteen. Kaushik Saravanan. https://www.kaushik.cv/blog/psg-hostel-wifi-automation