- Published on
Ugra CTF Quals 2022 – Хохорейсинг
- Authors
- Name
- thebish0p
- Description
- Security, Programming and Mathematics
xorxoracing
, 100)
Хохорейсинг (by Kalan
If you've already played enough Picky Snake, it's time to switch to another game, simpler.
We start by checking the web page and we can see that we can input an Encryption Key. We input test
as key and we get an encrypted text:
We have a new input field available Original Text
so the whole point is guessing the original text. After analysing the HTML source code we can see a js file that explain how the whole game works. Basically we input a key and get the ciphertext, however that ciphertext is damaged because we will have many characters hidden (replaced).
const socket = new WebSocket(`${window.location.href.replace('http', 'ws')}ws`);
const onClose = e => {
};
const onError = e => {
};
const onMessage = e => {
try {
data = JSON.parse(e.data);
} catch (_) {
// invalid data from server
return;
}
if (data.flag !== undefined) {
document.getElementById("flag").innerText = data.flag;
if (data.flag.length >= 70) {
document.getElementById("progress").className = "progress done";
}
}
if (data.countdown !== undefined) {
document.getElementById("timer").innerText = Math.floor(data.countdown / 10) + "" + (data.countdown % 10);
}
if (data.ciphertext !== undefined) {
document.getElementById("ciphertext").innerText = data.ciphertext;
document.getElementById("ciphertext").innerHTML = document.getElementById("ciphertext").innerHTML.replace(/Р–/g,
"<img class=eeee src=/static/err.svg style=width:1ch alt=err>");
document.getElementById("seg-1").className = "segment active";
document.getElementById("seg-2").className = "segment";
document.getElementById("text").disabled = false;
document.getElementById("submit-text").disabled = false;
document.getElementById("key").disabled = true;
document.getElementById("submit-key").disabled = true;
document.getElementById("text").focus();
}
if (data.text !== undefined) {
document.getElementById("text").value = data.text;
}
if (data.status !== undefined) {
document.getElementById("text").parentNode.className = "field " + data.status;
if (data.status) {
document.getElementById("seg-1").className = "segment";
document.getElementById("seg-2").className = "segment";
document.getElementById("text").disabled = true;
document.getElementById("submit-text").disabled = true;
document.getElementById("key").disabled = true;
document.getElementById("submit-key").disabled = true;
} else {
document.getElementById("seg-1").className = "segment";
document.getElementById("seg-2").className = "segment active";
document.getElementById("text").disabled = true;
document.getElementById("submit-text").disabled = true;
document.getElementById("key").disabled = false;
document.getElementById("submit-key").disabled = false;
document.getElementById("key").focus();
document.getElementById("ciphertext").innerHTML = " ";
document.getElementById("text").value = "";
document.getElementById("key").value = "";
}
}
};
socket.onclose = onClose;
socket.onerror = onError;
socket.onmessage = onMessage;
document.getElementById("key").focus();
document.getElementById("submit-text").onclick = e => {
socket.send(JSON.stringify({"text": document.getElementById("text").value}));
};
document.getElementById("submit-key").onclick = e => {
socket.send(JSON.stringify({"key": document.getElementById("key").value}));
};
document.getElementById("text").onkeypress = e => {
if (e.keyCode == 13) {
document.getElementById("submit-text").onclick(e);
}
};
document.getElementById("key").onkeypress = e => {
if (e.keyCode == 13) {
document.getElementById("submit-key").onclick(e);
}
};
window.setInterval(() => {
let t = Math.max(0, parseInt(document.getElementById("timer").innerHTML) - 1);
document.getElementById("timer").innerHTML = Math.floor(t / 10) + "" + (t % 10);
}, 1000);
The whole communication is happening through websockets. We send a key → Receive a ciphertext → Send plaintext → Receive original plaintext to compare:
I wrote a small script at first to just print out plaintexts and I noticed that the length of each plaintext is 40 chars. And I notice something really interesting and that each sentence comes from the US constitution.
import json
from websocket import create_connection
ans = 'a'
key = 'test'
ws = create_connection("wss://xoxoracing.q.2022.ugractf.ru/cf04601aff9c10bd/ws")
for _ in range(10):
ws.recv()
ws.send(json.dumps({"key": key}))
ciphertext_data = ws.recv()
ct = json.loads(ciphertext_data)['ciphertext']
ws.send(json.dumps({"text":ans}))
plaintext_data = ws.recv()
pt = json.loads(plaintext_data)['text']
print(pt)
print('============================================')
ws.close()
Output:
s pass any Bill of Attainder ex post fac
============================================
ations made by Law and a regular Stateme
============================================
subject to the jurisdiction thereof for
============================================
y subsequent Term of ten Years in such M
============================================
rect Taxes shall be apportioned among th
============================================
e the Adoption of this Constitution shal
============================================
cipation of any slave but all such debts
============================================
or proposing Amendments which in either
============================================
o construed as to affect the election or
============================================
Okay so the plaintexts are from the US consitution and don’t have any punctuation or new lines. Now let’s try to input 40 chars key → Xor it with ciphertext and see how it looks.
import json
from websocket import create_connection
ans = 'a'
key = 'a'*40
ws = create_connection("wss://xoxoracing.q.2022.ugractf.ru/cf04601aff9c10bd/ws")
for _ in range(10):
ws.recv()
ws.send(json.dumps({"key": key}))
ciphertext_data = ws.recv()
ct = json.loads(ciphertext_data)['ciphertext']
xor_list = [chr(ord(a) ^ ord(b)) for a,b in zip(ct, key)]
expected_pt = ''.join(xor_list).replace('ѷ', 'X')
ws.send(json.dumps({"text":ans}))
plaintext_data = ws.recv()
pt = json.loads(plaintext_data)['text']
print(expected_pt)
print(pt)
print('='*40)
ws.close()
Output:
X NX XXX XXXXXXX XXX XXXXXXXXXXXX XXX XX
n No law varying the compensation for th
========================================
XX X RXXXXXXXXXXXXX XXX XXXXX XXX XXXX
be a Representative who shall not have
========================================
XXXX XX XXXXXX X LXX XX XXXXXXXXX XX XXX
fore it become a Law be presented to the
========================================
XXX XXXXXXXXXXX XXX XXXXXX TXXX XXXXXXXX
the legislature may direct This amendmen
========================================
X XXXXXX RXXXXXXXXXXXXXX XXX XXXXXX TXXX
e chosen Representatives and direct Taxe
========================================
XXX XX XXXXX XX VXXXPXXXXXXXX XXXXX XX X
ber of votes as VicePresident shall be t
========================================
XXX XX XXXX XXXXXXXXX XXXXXXXXXXXX XXXXX
eof to make temporary appointments until
========================================
XXXX XXXXXX XXXX XX CXXXX XX RXXXXXXXX X
nded unless when in Cases of Rebellion o
========================================
X XXXXXX MXXXXXXXX XXX CXXXXXX JXXXXX XX
r public Ministers and Consuls Judges of
========================================
XXXXXXX XXXXXXX XX XXXXXXXXX XX XXX CXXX
hteenth article of amendment to the Cons
========================================
At this point I came up with the solution. Just convert the US constitution to a txt file, remove all new lines, remove new lines and just replace X
with a .
which would create a valid regex pattern then use that to get the plaintext and send it to the server. I wrote the following script and the output reveal the flag:
import json
import re
from websocket import create_connection
g = {}
ans = 'a'
ws = create_connection("wss://xoxoracing.q.2022.ugractf.ru/cf04601aff9c10bd/ws")
with open('final.txt', 'r') as f:
final = f.read()
while 1:
ws.recv()
key = "a"*40
ws.send(json.dumps({"key": key}))
ciphertext_data = ws.recv()
ct = json.loads(ciphertext_data)['ciphertext']
xor_list = [chr(ord(a) ^ ord(b)) for a,b in zip(ct, key)]
pattern = ''.join(xor_list).replace('ѷ', '.')
try:
ans = re.findall(pattern, final)[0]
except:
ans = 'miaw'
ws.send(json.dumps({"text":ans}))
plaintext_data = ws.recv()
pt = json.loads(plaintext_data)['text']
flag = json.loads(plaintext_data)['flag']
if flag != '':
print(flag)
Output revealing the flag:
ug
ugra_go_
ugra_go_go
ugra_go_go
ugra_go_go_go_co
ugra_go_go_go_co
ugra_go_go_go_come_o
ugra_go_go_go_come_o
ugra_go_go_go_come_on_yes
ugra_go_go_go_come_on_yes_yes
ugra_go_go_go_come_on_yes_yes_a_bit
ugra_go_go_go_come_on_yes_yes_a_bit_mor
ugra_go_go_go_come_on_yes_yes_a_bit_more_jus
ugra_go_go_go_come_on_yes_yes_a_bit_more_just_a_
ugra_go_go_go_come_on_yes_yes_a_bit_more_just_a_
ugra_go_go_go_come_on_yes_yes_a_bit_more_just_a_lit
ugra_go_go_go_come_on_yes_yes_a_bit_more_just_a_lit
ugra_go_go_go_come_on_yes_yes_a_bit_more_just_a_lit
ugra_go_go_go_come_on_yes_yes_a_bit_more_just_a_little_c
ugra_go_go_go_come_on_yes_yes_a_bit_more_just_a_little_c87b
ugra_go_go_go_come_on_yes_yes_a_bit_more_just_a_little_c87bce6b
ugra_go_go_go_come_on_yes_yes_a_bit_more_just_a_little_c87bce6bbf6daa
ugra_go_go_go_come_on_yes_yes_a_bit_more_just_a_little_c87bce6bbf6daa2
ugra_go_go_go_come_on_yes_yes_a_bit_more_just_a_little_c87bce6bbf6daa2
This is actually an unintended solution. You can read the intended solution here: https://github.com/teamteamdev/ugractf-2022-quals/blob/master/tasks/xoxoracing/WRITEUP.md