//程序启动时进行表结构和数据初始化
@PostConstruct
public void init() {
//删除表
jdbcTemplate.execute("drop table IF EXISTS `userdata`;");
//创建表,不包含自增ID、用户名、密码三列
jdbcTemplate.execute("create TABLE `userdata` (\n" +
" `id` bigint(20) NOT NULL AUTO_INCREMENT,\n" +
" `name` varchar(255) NOT NULL,\n" +
" `password` varchar(255) NOT NULL,\n" +
" PRIMARY KEY (`id`)\n" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
//插入两条测试数据
jdbcTemplate.execute("INSERT INTO `userdata` (name,password) VALUES ('test1','haha1'),('test2','haha2')");
}
@Autowired
private JdbcTemplate jdbcTemplate;
//用户模糊搜索接口
@PostMapping("jdbcwrong")
public void jdbcwrong(@RequestParam("name") String name) {
//采用拼接SQL的方式把姓名参数拼到LIKE子句中
log.info("{}", jdbcTemplate.queryForList("SELECT id,name FROM userdata WHERE name LIKE '%" + name + "%'"));
}
发送以下请求:
http://localhost:45678/sqlinject/jdbcwrong?name=test
这个接口的 name 参数有两种可能的注入方式:一种是报错注入,一种是基于时间的盲注
下面我们用尝试一下直接导出整个用户表的内容:?
python sqlmap.py -u http://localhost:45678/sqlinject/jdbcwrong --data name=test --current-db
发现当前的数据库是common_mistakes;
current database: 'common_mistakes'
接下来我们通下面的语句
python sqlmap.py -u http://localhost:45678/sqlinject/jdbcwrong --data name=test --tables -D "common_mistakes" ?
?
Database: common_mistakes
[7 tables]
+---------------------------+
| user |
| common_store |
| hibernate_sequence |
| m |
| news |
| r |
| userdata |
+---------------------------+
接下来我们就可以导出userdata表中的数据了:
python sqlmap.py -u http://localhost:45678/sqlinject/jdbcwrong --data name=test -D "common_mistakes" -T "userdata" --dump
我们发现用户名和密码全找到了
Database: common_mistakes
Table: userdata
[2 entries]
+----+-------+----------+
| id | name | password |
+----+-------+----------+
| 1 | test1 | haha1 |
| 2 | test2 | haha2 |
+----+-------+----------+
所谓盲注,指的是注入后并不能从服务器得到任何执行结果(甚至是错误信息),只能寄希望服务器对于 SQL 中的真假条件表现出不同的状态。比如,对 于布尔盲注来说,可能是“真”可以得到 200 状态码,“假”可以得到 500 错误状态码;或者,“真”可以得到内容输出,“假”得不到任何输出。总之,对于不 同的 SQL 注入可以得到不同的输出即可。在这个案例中,因为接口没有输出,也彻底屏蔽了错误,布尔盲注这招儿行不通了。那么退而求其次的方式,就 是时间盲注。也就是说,通过在真假条件中加入 SLEEP,来实现通过判断接口的响应时间,知道条件的结果是真还是假。基于时间的盲注原理就是,虽然 不能直接查询出 password 字段的值,但可以按字符逐一来查,判断第一个字符是否是 a、是否是 b……,查询到 h 时发现响应变慢了,自然知道这就是真 的,得出第一位就是 h。以此类推,可以查询出整个值。在代码中,我们可以引入p6spy工具打印出所有执行的 SQL,观察 sqlmap 构造的一些 SQL,来分析 其中原理。
<dependency>
<groupId>com.github.gavlyukovskiy</groupId>
<artifactId>p6spy-spring-boot-starter</artifactId>
<version>1.6.1</version>
</dependency>
解决方式还是使用参数化查询,让任何外部输入值只可能作为数据来处理。比如,对于之前那个接口,在 SQL 语句中使用“?”作为参数占位符,然后提 供参数值。这样修改后,sqlmap 也就无能为力了。
@PostMapping("jdbcright")
public void jdbcright(@RequestParam("name") String name) {
log.info("{}", jdbcTemplate.queryForList("SELECT id,name FROM userdata WHERE name LIKE ?", "%" + name + "%"));
}
对于 MyBatis 来说,同样需要使用参数化的方式来写 SQL 语句。在 MyBatis 中,“#{}”是参数化的方式,“${}”只是占位符替换
@Select("SELECT id,name FROM `userdata` WHERE name LIKE '%${name}%'") //这个是替换不是参数化
List<UserData> findByNameWrong(@Param("name") String name);
正确做法:
@Select("SELECT id,name FROM `userdata` WHERE name LIKE CONCAT('%',#{name},'%')") List findByNameRight(@Param("name") String name);
再看使用in的一个例子
<select id="findByNamesWrong" resultType="org..time.commonmistakes.codeanddata.sqlinject.UserData">
SELECT id,name FROM `userdata` WHERE name in (${names}) //这种做法同样会有注入
</select>
@PostMapping("mybatiswrong2")
public List mybatiswrong2(@RequestParam("names") String names) {
return userDataMapper.findByNamesWrong(names);
}
python sqlmap.py -u http://localhost:45678/sqlinject/mybatiswrong2 --data names="'test1','test2'"
正确做法:
@PostMapping("mybatisright2")
public List mybatisright2(@RequestParam("names") List<String> names) {
return userDataMapper.findByNamesRight(names);
}
<select id="findByNamesRight" resultType="org..time.commonmistakes.codeanddata.sqlinject.UserData"> SELECT id,name FROM `userdata` WHERE name in <foreach collection="names" item="item" open="(" separator="," close=")"> #{item} </foreach></select>