Doshisha SSO emoji autocomplete hack
私たちの研究室
アドベントカレンダー23日目〜
Doshisha SSO emoji autocomplete hack
Article by エスカーニョ マルケス、ユイス
The Doshisha Single Sing On has multiple two factor mechanisms to login into its ecosystem. In this article I will show you how I automated the emoji challenge to save a few seconds every time I log in. This is yet another example of how engineers like to invest hours of work into automating a five seconds task, just because they can.
TL;DR:
We analyze the Doshisha SSO webpage, see the patterns that allow us to hard-code the emoji PIN in a JavaScript script. And then we program a quick code to automatically press the buttons for us, which we put it inside Greasemonkey to do this every time we access the page.
My problem
Every day I have to log into my Doshisha account to access multiple student services. Sometimes I will do so at home, other times at the laboratory, at class or even at the library. My problem is that the Doshisha emoji challenge makes it very obvious when you are selecting the emojis, and anyone around you can easily see your emoji pattern. This makes it a bit less secure and highly inconvinient.
To showcase the problem, the login page has two steps. First you input your username and password:
Secondly, you are prompted to select your preferred Multi-Factor Authentication.
Of the presented options, the only one that worked for me was Imaging Matrix, which consists of selecting three emoji characters that you set up during the account creation (and can be changed from the settings):
Every time you press a over an Emoji icon, a gray background will flash for a hundred milliseconds, making your click very obvious to you, and anyone looking at your screen:
So I decided to modify the login page to adapt it to my preferences.
Proposed solution
My solution is simple, instead of having to manually select the icons, I will let the browser autocomplete it for me. To do so, I will inject a JavaScript code in the website to automatically click the icons for me.
Hacking around
First and foremost, let's understand how the page functions.
Understanding the Imaging Matrix page
The Imaging Matrix page is dynamically created through a JavaScript code that builds the table with the Emojis. Once the page has loaded and the initial code finishes creating the elements, we can take a look at how it is structured with the browser debugger.
<div id="imatrix_div" style="width: 100%; height: 360px;">
<div class="digit_label_class" style="top:0px;left:0px;"></div>
<div style="position:absolute;top:45px;left:0px;width:65px;height:45px;" class="digit_label_class">40</div>
<!-- [...] MORE LABELS -->
<div id="button0" class="input_imgdiv_class" style="top:45px;left:65px;background-image:url('/idp/tenant/0/images/imatrix/e6.gif')" onclick="image_button_click_div('button0', '4042');"></div>
<!-- [...] MORE BUTTONS -->
<div align="left" id="panel_field" style="top:270px;left:0px;width:490px;height:30px;background-color:#DCDCDC">
<input type="password" id="input_digit_0" class="input_digit_class" maxlength="10" style="border-style:none; background-color:transparent;" autocomplete="off" tabindex="-1" readonly="">
<!-- [...] TOTAL OF 4 IMPUTS -->
</div>
<div align="right" id="button_panel" style="top:308px;width:490px;height:30px;">
<input type="button" style="width:90px;" id="btnLogin" value="Login" onclick="btnLogin_Click();">
<input type="button" style="width:90px;" id="btnClear" value="Clear" onclick="btnClear_Click();">
<input type="button" style="width:90px;" id="btnPanelHyouji" value="Show Panel" onclick="btnPanelHyouji_Click();">
</div>
</div>
We have quite some information in this HTML snipped. We don't care about the labels, but we can see how the buttons are formed, and that they call image_button_click_div
, with some 4 digit numeric code. Moreover, there also are some inputs where the password pieces will be coupled together. Finally there are the control buttons that allow to login, clear and the "Show Panel" action.
We see the following useful JavaScript function:
- btnLogin_Click → Launches the login process once the Emoji's have been inputted.
- btnClear_Click → Clears the inputs that contain the inputted Emoji's codes.
We will save it for the future, now we will focus on understanding the Emoji codes that get passed on image_button_click_div
. This four-digit codes are hard to guess, because if we try to log in again, we will get a different code for the same Emoji character, which makes it hard to guess what code to send to the function. We could inspect the JavaScript code and try to untangle how is this code generated, but it gets complex quite quickly when you enter some cryptographic functions.
However, there is a constant all the time, the Emoji characters are always the same, I always have to press the exact same 3 emojis in a specific order.
Taking a closer look to the emoji buttons:
<div id="button0" class="input_imgdiv_class" style="top:45px;left:65px;background-image:url('/idp/tenant/0/images/imatrix/e6.gif')" onclick="image_button_click_div('button0', '4042');"></div>
<div id="button1" class="input_imgdiv_class" style="top:45px;left:150px;background-image:url('/idp/tenant/0/images/imatrix/e22.gif')" onclick="image_button_click_div('button1', '4046');"></div>
<div id="button2" class="input_imgdiv_class" style="top:45px;left:235px;background-image:url('/idp/tenant/0/images/imatrix/e10.gif')" onclick="image_button_click_div('button2', '4044');"></div>
There is a clear pattern to recognize the Emoji, the style background-image property always has the same URL for the same Emoji icon. More precisely, the use the GIF image's names: e6.gif
, e22.gif
, and e10.gif
; all start with an e
, followed by a number between 1 and 24 (included), and then the .gif
extension.
Browser debugger hack
Now that we know enough about the page internals, we can develop our first prototype in the browser's console, to get a first iteration of your autocomplete script.
First we find our emoji icon numbers by inspecting the button element and reading the gif image URL:
Preparations
For our demonstration, let's assume that our icons are 📷️🐫🍉 (e1.gif
, e24.gif
, and e11.gif
). So we declare it as a constant:
const CODE=['1', '24', '11']; // gif codes
Then we need to find the button elements containing those codes. To do so we declare two functions:
const Q = (x) => Array.from(document.querySelectorAll(x));
const I = (img) => Q('.input_imgdiv_class').filter(y => y.style.backgroundImage.indexOf(`/e${img}.gif`) >=0 )[0];
-
Q(x) will simply return an array of the
querySelectorAll
operator, which allows us to filter elements by id:Q('#myid')
, by classQ('.myclass')
or other standard selectors. -
I(img) will return the button element of the given emoji code (img). For example
I('1')
will return the DOM button element for the emoji withe1.gif
URL.
Action
After declaring the helper functions, we are ready to jump into action. The first thing we will do, just in case is to clear the patterns. Remember the "btnClear_Click" function from the page source code? This will be straight forward:
window.btnClear_Click();
With a clear base, we type the code by pressing the buttons:
CODE.map(I).map((e) => {
e.onclick();
});
And after running this code in the console, we already get the code completed.
Avoid flashing
However, we still have the discretion problem that creates the fact that the emoji button flashes with a gray background when pressed. Let's inspect the function image_button_click_div
:
function image_button_click_div(button_id, label_str) {
if (IMAGE_CLICK_MODE == 1) {
image_button_click_common(label_str);
var el = document.getElementById(button_id);
if(el) {
el.style.backgroundColor = IMAGE_BUTTON_COLOR;
setTimeout(
"var e = document.getElementById('" + button_id + "'); " +
"if(e) { e.style.backgroundColor = 'white';}",
DELAY_TIME);
}
}
}
This is the full function code extracted through the debugger. We can clearly see how the button element is captured through document.getElementById(button_id);
and then it's background changes to IMAGE_BUTTON_COLOR
, waits DEALY_TIME
and goes back to 'white'
. Now, we could use multiple techniques to avoid this change, but for sake of simplicity, we will just make the folllowing line fail:
var el = document.getElementById(button_id);
Since the next line immediately checks if the button retrieval was successful, good coding practice, we can make sure the button_id is wrong, and so the animation will not trigger. To do so, we change the button id before clicking it:
code.map(i).map((e) => {
e.id = e.id + 'x';
e.onclick();
e.id = e.id.replace('x','');
});
Simple but effective.
Making the login button bigger
This step is not necessary, but since we are at it, we could make the login button bigger, so it's easier to press. Doing so it's quite trivial, we just modify some style properties:
const b = Q('#btnLogin')[0];
b.style.padding = '14px';
b.style.fontSize = '24px';
b.style.width = '100%';
And we will get a big and easy to click button:
Optionally, if you don't like to click with your mouse, you can also just call the login function directly:
window.btnLogin_Click();
But I prefer to have the option to see the page and be able to react, even if it's just pressing a big "Login" button.
Greasemonkey
With the code working in the browser's console, it's time to move it to production. We could create our own custom add-on, but that's a lot of work for a quick and simple script, so we will make use of Greasemonkey. Which is a browser add-on that let's us create custom scripts for webpages.
We have to add a few extras to our script to make it suitable for Greasemonkey. We need a header declaring a few properties:
// ==UserScript==
// @name Doshisha Login ImageMatrix
// @version 1
// @grant none
// @include /^https://idp.doshisha.ac.jp/idp/Authn/External.*/
// @author LluísE
// @run-at document-end
// ==/UserScript==
The important part is the @include
, which contains a RegEx expression to only execute the code in the matching pages.
Moreover, Greasemonkey does not run the code in the webpage, but rather at the browser level, so we could access restricted APIs such as managing Tabs, controlling storage without restrictions... but for our simple script we don't need any of that functionality, and we have the problem that for security, Greasemonkey will run our script in a sandbox. This sandbox blocks us from interacting with the page JavaScript functions.
The simplest workaround is to run a code that injects our script as website code by creating a new <script>
element:
function INYECT_ONLOAD(fn) {
window.addEventListener("DOMContentLoaded", (_) => {
const d = document, s = d.createElement('script');
s.setAttribute("type","application/javascript");
s.textContent='(' + fn + ')();';
d.body.appendChild(s);
d.body.removeChild(s)},
false
)
}
Finally, there still is a last problem, and that's the fact that the login page is dynamically created with JavaScript, so by the time our code runs, it might still be to early, and the emoji buttons might not be created. A simple solution is to wait until the page script has done its job:
async function() {
const delay = ms => new Promise(res => setTimeout(res, ms));
while (!window.btnClear_Click || !window.arrScreenSettingInfo)
await delay(100);
// Rest of our code
}
That way, our code will run at the correct moment.
Results
Success! Every time my browser visits the emoji challenge page, I see this:
With the pin automatically completed for me, so I can click the big Login button and go on with my day.
Conclusion
To sum up, in this article we learned how to inspect a webpage to understand its internals, and how to hack around to modify its behavior and appearance to our preference. To facilitate the execution of our scripts, we used Greasemonkey to automatically inject our code.
Full Greasemonkey code
// ==UserScript==
// @name Doshisha Login ImageMatrix
// @version 1
// @grant none
// @include /^https://idp.doshisha.ac.jp/idp/Authn/External.*/
// @author LluísE
// @run-at document-end
// ==/UserScript==
function INYECT_ONLOAD(fn) {window.addEventListener("DOMContentLoaded", _=>{const d=document,s=d.createElement('script');s.setAttribute("type","application/javascript");s.textContent='('+fn+')();';d.body.appendChild(s);d.body.removeChild(s)},!1)}
INYECT_ONLOAD(async function() {
// Check if page loaded because Doshisha inyects whole HTML in a JS script for some reason and onload event is not enough
const delay = ms => new Promise(res => setTimeout(res, ms));
while (!window.btnClear_Click || !window.arrScreenSettingInfo)
await delay(100);
// THE CODE:
const CODE=['1', '24', '11'];
const Q=x=>Array.from(document.querySelectorAll(x));
const I=img=>Q('.input_imgdiv_class').filter(y=>y.style.backgroundImage.indexOf(`/e${img}.gif`)>=0)[0];
window.btnClear_Click();
CODE.map(I).map(e=>{
e.id=e.id+'x';
e.onclick();
e.id=e.id.replace('x','');
});
const b=Q('#btnLogin')[0];
b.style.padding='14px';
b.style.fontSize='24px';
b.style.width='100%';
// window.btnLogin_Click();
});
The end
Article by エスカーニョ マルケス、ユイス
▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁
░▇▇▇▇▇▇▇▇▇░░░░░░░░░░░░░▇▇▇░░░▇▇▇▇▇▇▇▇▇▇▇░
░░░░░░░░▇▇░░░░░░░░▇▇▇▇▇░░░░░░░░░░░░░░▇▇░░
░░░░░░░░▇▇░░░░░▇▇▇░░▇▇░░░░░░░░░░░░░▇▇░░░░
░░░░░░░░▇▇░░░░░░░░░░▇▇░░░░░░░░░░░▇▇▇░░░░░
░░░░░░░░▇▇░░░░░░░░░░▇▇░░░░░░░░░▇▇░░░▇▇░░░
░▇▇▇▇▇▇▇▇▇▇▇░░░░░░░░▇▇░░░░░░░▇▇░░░░░░░▇▇░
▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
Discussion