一、写在前面
好久没有写技术文章了,今天,想给大家推荐一款我最近精心设计研发的代码安全分析扫描器,名叫Artemis。这款代码安全扫描器融合了经典的代码安全分析技术以及最新的大模型分析技术,能够帮助各位进行更加深入的代码安全分析工作,发现更多代码安全问题。(下载链接可从文末获取)
二、Artemis静态代码分析技术栈:数据流、编译时捕获、控制流分析、漏洞分析、AI智能分析
2.1 编译时捕获
Artemis静态代码分析工具借鉴了coverity、codeql等一些知名代码安全分析工具的实现思路,采用编译时进行一些语义信息的捕获,比如类型推导信息。根据多年来的静态代码实践经验来看,静态代码分析的准确性,很大程度上和类型推导的准确性相关,尤其是在过程间分析的时候,如果不能够很好的识别函数所属对象的类型,那调用链构建方面将会有很大的误差。很多开源白盒基于ast遍历分析推导类型信息,这种方式的弊端是,一方面需要花费很多时间在解析程序的编写上,以适配很多的场景;另一方面,针对第三方库的函数,由于不能对其声明进行解析识别,导致我们单从源码层层面无法很准确的推断出他的函数签名信息,进而导致误报加剧。其实,编译器已经帮助我们推导了所需的大多数类型信息,而且他推导的准确性肯定是高于开源社区方案推导的准确性,因为编译器他推导不成功的话,那程序根本跑不起来。
2.2 数据流及控制流
artemis在编译阶段,会基于提取的ast信息,构建项目代码的控制流信息,并基于此,进一步解析各个过程的数据流指向关系。所有编译时提取的语义信息都持久化存储在源码文件对应的编译产出物中。当然,这其中只涉及过程内的语义信息,至于过程间的语义信息,artemis是放在漏洞分析过程中进行动态分析的。
2.3 漏洞分析
针对漏洞分析方面,artemis的通用漏洞检测策略,比如针对数据流相关的漏洞检测方案,主要是通过规则定位相关的sink点和source点,然后基于数据流指向信息以及调用链信息推断source点到达sink点的可能路径,这一思路也会业内普遍使用的分析思路。为了实现这个方案,artemis参考了codeql及joern的查询脚本的设计思路,设计了一款属于artemis自己的ql语法,名叫artQL,安全工程师可通过artQL灵活的编写安全分析策略。
以下是Artemis漏洞分析的原理示意图
2.4 AI智能分析
Artemis一个比较亮眼的功能就是能够在规则层面提供直接调用大模型进行代码安全检测规则编写,发现更深层次的问题。具体实现原理以及应用实践,请移步第四个章节。
三、artQL
为了让读者能够更易于理解后续的一些章节,我先简要的介绍下artQL的语法。下面是artQL比较重要的关键词:
关键字 | 说明 | 示例 |
---|---|---|
db | 数据库,所有的数据都从这来 | |
call | 函数调用 | db.call |
method | 函数声明 | db.method |
types | 类型声明 | db.types |
xml | xml文件信息 | db.xml |
where | 条件判断 | db.call.where(_.name=="niceshot") |
whereNot | 条件判断 | db.call.whereNot(_.name=="nicejob") |
filter | 条件过滤 | db.call.filter(_.name=="nicejob") |
limit | 枚举限制,限制获取的节点数量 | db.call.limit(10) |
methodDeclaration | 获取函数调用对应的函数声明信息 | callExpr.methodDeclaration |
hasPathTo | 污点路径追踪 | sink.hasPathTo(source) |
以上是对于规则编写比较帮助的关键字信息,基于关键字信息介绍以及示例demo,相比你已经知道如何去编写一个基础的安全检测规则了,例如我们可以编写一个sql注入检测的规则:
val sources = db.call.where(_.fullName=="javax.servlet.http.HttpServletRequest.getParameter")
val sink=db.call.where(_.fullName=="org.springframework.jdbc.core.JdbcTemplate.update").filter(_.arguments.nonEmpty).map(_.arguments.head)
val paths=sink.hashPathTo(sources)
paths.show //打印路径
paths.num //统计路径数量
四、实际项目分析
4.1 通用漏洞分析 - ofcms漏洞分析
4.1.1 ofcms sql注入分析
漏洞说明
是骡子是马,拿出来溜溜。下面我们来使用artQL来进行一些实际的安全问题的分析。我们就以ofcms这个项目来作为例子。
根据cve:https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-9615
描述,ofcms在1.1.3版本之前存在大量的sql注入漏洞。
具体位置
通过源码分析,我们发现ofcms是基于jfinal框架进行开发,且sql操作也是基于jfinal提供的api进行处理。例如:ofcms-admin/src/main/java/com/ofsoft/cms/admin/controller/system/SystemGenerateController.java
public void create() {
try {
String sql = getPara("sql");
Db.update(sql);
rendSuccessJson();
} catch (Exception e) {
e.printStackTrace();
rendFailedJson(ErrorCode.get("9999"), e.getMessage());
}
}
该源码大致意思是从网络请求参数中获取sql参数,然后调用Db.update进行数据更新操作。
这个Db,通过跟踪分析,其全路径是:com.jfinal.plugin.activerecord.Db。
这里直接将外部传入的sql语句进行执行,很明显是存在sql注入漏洞的。那么如何用artemis检测该漏洞呢?我们可以编写如下规则:
<c-rule>
<id>dataflow-rule-sqli</id>
<name>sql注入漏洞</name>
<enabled>true</enabled>
<script>
//示例
def sinks:List[Expr]=db.call.where(_.fullName=="java.sql.Statement.executeQuery").map(_.arguments.head)
def sources = com.pony.rule.sources.HttpSources.http_entry
def paths=sinks.hasPathTo(sources)
paths.save(name = "sql注入漏洞", desc = "sql注入漏洞",level = "high", rule_id = "dataflow-rule-sqli")
</script>
</c-rule>
简单解读写规则,该规则中source点为jfinal框架的网络请求入口点getPara或getParamsMap,sink点是进行渲染操作的render函数,如果需要精确的识别api,可使用fullname指定api的全限定名。我们将规则加入到我们的Checker.xml规则文件中,然后下面我们正式进行代码分析测试。
首先,由于每一个项目需要的jdk和编译命令可能都不太一样,所以这里需要我们先配置下项目编译需要的相关参数。这个参数在conf目录下的build.properties中配置即可。
具体在build.properties文件中,我们重点关注以下配置
java_home=/Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home
report_output=report.json
build_cmd= mvn package -X -DskipTests=true
out_dir=workspace
其中,java_home是我们jdk的地址,report_output是我们报告输出的地址,build_cmd是我们的项目编译命令,out_dir是中间语义数据库输出的目录
配置好后,下面我们执行以下命令即可进行编译捕获
从命令行执行日志可以发现,捕获449个源码大概耗时55秒,这个速度应该是可以接受的。
下面我们执行以下命令进行漏洞分析:java -jar Artemis.jar -d workspace
可以发现Artemis共发现了43个sql注入漏洞,8个模版注入,两个文件操纵漏洞和一个文件上传漏洞。
其中我们刚才分析的那个sql注入漏洞的检测结果如下:
{
"name": "sql注入漏洞",
"checker_name": "dataflow-rule-sqli",
"trace": [{
"desc": "",
"line": 48,
"code": "sql",
"filePath": "/Users/pony/Desktop/release/1.0.0/test/ofcms-V1.1.2/ofcms-admin/src/main/java/com/ofsoft/cms/admin/controller/system/SystemGenerateController.java"
}, {
"desc": "",
"line": 47,
"code": "sql",
"filePath": "/Users/pony/Desktop/release/1.0.0/test/ofcms-V1.1.2/ofcms-admin/src/main/java/com/ofsoft/cms/admin/controller/system/SystemGenerateController.java"
}, {
"desc": "",
"line": 47,
"code": "getPara(\"sql\")",
"filePath": "/Users/pony/Desktop/release/1.0.0/test/ofcms-V1.1.2/ofcms-admin/src/main/java/com/ofsoft/cms/admin/controller/system/SystemGenerateController.java"
}],
"desc": " SQL注入(SQL Injection)是一种针对Web应用程序的攻击技术。具体来说,它是指web应用程序对用户输入数据的合法性没有判断或过滤不严,攻击者可以在web应用程序中事先定义好的查询语句的结尾上添加额外的SQL语句,在管理员不知情的情况下实现非法操作,以此来实现欺骗数据库服务器执行非授权的任意查询,从而进一步得到相应的数据信息。\u0026#xA; SQL注入攻击通常利用了应用程序没有对用户输入数据进行充分验证和过滤的漏洞,使得攻击者可以将恶意的SQL语句注入到应用程序的输入框或URL中,从而绕过应用程序的身份验证和访问控制机制,进而执行恶意操作,如获取敏感数据、修改数据、删除数据等。\u0026#xA; ",
"level": "high"
}
4.1.2 ofcms 模版注入漏洞分析
上个章节中,我们在使用Artemis对ofcms项目进行sql注入扫描的时候,发现在扫描的过程中也发现了一些服务端模版注入漏洞。通过对报告的review,发现主要问题集中在文件com/ofsoft/cms/front/controller/IndexController.java
及com/ofsoft/cms/front/controller/ComnController.java
中,下面我们以IndexController.java中的ssti漏洞为例子,讲解下Artemis是如何发现这个漏洞的。
package com.ofsoft.cms.front.controller;
import com.jfinal.core.ActionKey;
import com.jfinal.plugin.activerecord.Record;
import com.ofsoft.cms.core.annotation.Action;
import com.ofsoft.cms.core.config.AdminConst;
import com.ofsoft.cms.core.config.FrontConst;
import com.ofsoft.cms.core.uitle.SiteUtile;
import java.util.Map;
/**
* 页面配置
*
* @author OF
* @date 2018年1月2日
*/
@Action()
public class IndexController extends BaseController {
/**
* 首页页面
*/
@ActionKey(value = "index")
public void front() {
render(FrontConst.TEMPLATE_PATE + SiteUtile.getTemplatePath() + "/index.html");
}
/**
* 管理台首页默认跳转
*/
@ActionKey(value = "/admin")
public void admin() {
redirect(AdminConst.indexHtml);
}
/**
* 首页面配置
*/
@ActionKey(value = "/")
public void index() {
Map params = getParamsMap();
String page = getPara(0);
//是否是首页
if ("/".equals(page) || page == null || "index".equals(page)) {
setAttr("site", SiteUtile.getSite());
render(FrontConst.TEMPLATE_PATE + SiteUtile.getTemplatePath() + "/index.html");
return;
}
//获取当前栏目
params.put("site_id", SiteUtile.getSiteId());
params.put("column_english", page);
params.put("page", page);
Record record = SiteUtile.getColumn(params);
String isContent = getPara(1);
if (record == null) {
if ("c".equals(isContent)) {
params.put("content_id", getParaToInt(2, 0));
setAttr("params", params);
render(FrontConst.TEMPLATE_PATE + SiteUtile.getTemplatePath() + "/article.html");
return;
}
render(FrontConst.TEMPLATE_PATE + SiteUtile.getTemplatePath() + FrontConst.pageError);
return;
}
setAttr("columns", record);
setAttr("params", params);
//是否是内容
if ("c".equals(isContent)) {
params.put("content_id", getParaToInt(2, 0));
String templatePath = SiteUtile.getTemplatePath(record.getStr("column_content_page"), "/article.html");
render(FrontConst.TEMPLATE_PATE + SiteUtile.getTemplatePath() + templatePath);
return;
}
//是否是单页
if ("1".equals(record.getStr("is_open"))) {
String templatePath = SiteUtile.getTemplatePath(record.getStr("template_path"), "/sing.html");
render(FrontConst.TEMPLATE_PATE + SiteUtile.getTemplatePath() + templatePath);
return;
}
//当前页码 栏目页
int pageNum = getParaToInt(1, 1);
setAttr("pageNum", pageNum);
String templatePath = SiteUtile.getTemplatePath(record.getStr("template_path"), "/list.html");
render(FrontConst.TEMPLATE_PATE + SiteUtile.getTemplatePath() + templatePath);
return;
}
/**
* 列表页面
*/
@ActionKey(value = "/list")
public void list() {
render(FrontConst.TEMPLATE_PATE + SiteUtile.getTemplatePath() + "/list.html");
}
/**
* 内容页面
*/
@ActionKey(value = "/content")
public void content() {
String p = getRequest().getRequestURI();
p = p.replace(getRequest().getContextPath(), "").replace("/content", "");
render(FrontConst.TEMPLATE_PATE + SiteUtile.getTemplatePath() + p);
}
/**
* 栏目页面
*/
@ActionKey(value = "/column")
public void column() {
Map params = getParamsMap();
String page = getPara(0);
//当前页码
int pageNum = getParaToInt(1, 1);
//获取当前栏目
params.put("site_id", SiteUtile.getSiteId());
params.put("column_english", page);
params.put("page", page);
Record record = SiteUtile.getColumn(params);
setAttr("columns", record);
setAttr("params", params);
setAttr("pageNum", pageNum);
String templatePath = record.getStr("template_path");
if (templatePath == null) {
templatePath = "index.html";
} else {
if (!templatePath.startsWith("/")) {
templatePath = "/" + templatePath;
}
if (!templatePath.endsWith(".html")) {
templatePath = templatePath + ".html";
}
}
render(FrontConst.TEMPLATE_PATE + SiteUtile.getTemplatePath() + templatePath);
}
/**
* 普通页面
*/
@ActionKey(value = "/news")
public void news() {
String p = getRequest().getRequestURI();
p = p.replace(getRequest().getContextPath(), "").replace("/page", "");
render(FrontConst.TEMPLATE_PATE + SiteUtile.getTemplatePath() + p);
}
/**
* 页面配置
*/
@ActionKey(value = "page")
public void page() {
String s = getPara("s");
if (s.lastIndexOf(".html") != 0) {
s = s + ".html";
}
setAttr("params", getParamsMap());
render(FrontConst.TEMPLATE_PATE + SiteUtile.getTemplatePath() + s);
}
}
通过源码分析,很显然,此处的ssti注入漏洞的是误报的,因为其并不能直接控制ssti模版内容,只能控制选择哪一个ssti模版。
我们看下我们之前的artQL规则是怎么写的:
<c-rule>
<id>dataflow-rule-ssti-ofcms</id>
<name>ssti注入漏洞</name>
<enabled>true</enabled>
<script>
<![CDATA[
def source=com.pony.rule.sources.HttpSources.http_entry <br>
def sink=db.call.where(_.name=="render").filter(_.arguments.nonEmpty).map(_.arguments.head) <br>
def paths=sink.hasPathTo(source) <br>
paths.save(name = "ssti注入漏洞", desc = "ssti注入漏洞",level = "high", rule_id = "dataflow-rule-ssti-ofcms") <br>
]]>
</script>
</c-rule>
只进行了简单的数据流路径推断,没有更深入分析其传入的内容是否能够真正构成漏洞。
我们在原来的规则上进行进一步改造,增加一些文件上传逻辑的搜索,另外我们还需要将两者进行适当的关联比对,以减少误报的产生
<![CDATA[
def source=com.pony.rule.sources.HttpSources.http_entry <br>
def sink=db.call.where(_.name=="render").filter(_.arguments.nonEmpty).map(_.arguments.head) <br>
def paths=sink.hasPathTo(source) <br>
val sink_file_upload= db.call.where { <br>
it => <br>
it.fullName == "java.nio.file.Files.write" <br>
|| it.fullName == "java.io.FileOutputStream.write" <br>
}.filter(_.arguments.nonEmpty).map(_.arguments.head) <br>
val paths_file_upload=sink_file_upload.hasPathTo(sources = source) <br>
val final_path:ListBuffer[Path]=new ListBuffer[Path] <br>
if(paths.nonEmpty){ <br>
paths.foreach{ <br>
it=>{ <br>
val target:ListBuffer[AST]=new ListBuffer[AST] <br>
val head_node=it.nodes.head <br>
//提取其中的符号信息 <br>
val t=head_node.parent.node <br>
if(t.nonEmpty) { <br>
com.pony.dataflow.DataFlowAnalyze.find_target_expr(start = t.get, expr_Type = "NameExpr", targets = target) <br>
Breaks.breakable { <br>
target.foreach { <br>
t => { <br>
val t_name = t.asInstanceOf[NameExpr].name <br>
//对于每个文件上传流 <br>
val filtered_paths = paths_file_upload.filter { <br>
l => { <br>
l.nodes.count(_.code.contains(t_name)) > 0 <br>
} <br>
} <br>
if (filtered_paths.nonEmpty) { <br>
filtered_paths.foreach { <br>
f => { <br>
val p=new Path() <br>
p.nodes.addAll(f.nodes.reverse++it.nodes.reverse) <br>
final_path.addOne(p) <br>
} <br>
} <br>
Breaks.break() <br>
} <br>
} <br>
} <br>
} <br>
} <br>
} <br>
} <br>
} <br>
final_path.toList.save(name = "ssti注入漏洞", desc = "ssti注入漏洞",level = "high", rule_id = "dataflow-rule-ssti-ofcms") <br>
]]>
简单的描述下规则逻辑,第一步获取前面的那个ssti漏洞检测规则的路径,然后再获取项目中存在的文件上传数据流,然后通过关联分析,将两者路径进行merge。
下面是改造后的规则扫描的结果路径
{
"name": "ssti注入漏洞",
"checker_name": "dataflow-rule-ssti-ofcms",
"trace": [{
"desc": "",
"line": 121,
"code": "getRequest().getParameter(\"file_content\")",
"filePath": "/Users/pony/Desktop/release/1.0.0/test/ofcms-V1.1.2/ofcms-admin/src/main/java/com/ofsoft/cms/admin/controller/cms/TemplateController.java"
}, {
"desc": "",
"line": 121,
"code": "fileContent",
"filePath": "/Users/pony/Desktop/release/1.0.0/test/ofcms-V1.1.2/ofcms-admin/src/main/java/com/ofsoft/cms/admin/controller/cms/TemplateController.java"
}, {
"desc": "",
"line": 122,
"code": "fileContent",
"filePath": "/Users/pony/Desktop/release/1.0.0/test/ofcms-V1.1.2/ofcms-admin/src/main/java/com/ofsoft/cms/admin/controller/cms/TemplateController.java"
}, {
"desc": "",
"line": 122,
"code": "fileContent",
"filePath": "/Users/pony/Desktop/release/1.0.0/test/ofcms-V1.1.2/ofcms-admin/src/main/java/com/ofsoft/cms/admin/controller/cms/TemplateController.java"
}, {
"desc": "",
"line": 124,
"code": "fileContent",
"filePath": "/Users/pony/Desktop/release/1.0.0/test/ofcms-V1.1.2/ofcms-admin/src/main/java/com/ofsoft/cms/admin/controller/cms/TemplateController.java"
}, {
"desc": "",
"line": 74,
"code": "String string",
"filePath": "/Users/pony/Desktop/release/1.0.0/test/ofcms-V1.1.2/ofcms-admin/src/main/java/com/ofsoft/cms/core/uitle/FileUtils.java"
}, {
"desc": "",
"line": 78,
"code": "string",
"filePath": "/Users/pony/Desktop/release/1.0.0/test/ofcms-V1.1.2/ofcms-admin/src/main/java/com/ofsoft/cms/core/uitle/FileUtils.java"
}, {
"desc": "",
"line": 154,
"code": "getPara(\"s\")",
"filePath": "/Users/pony/Desktop/release/1.0.0/test/ofcms-V1.1.2/ofcms-admin/src/main/java/com/ofsoft/cms/front/controller/IndexController.java"
}, {
"desc": "",
"line": 154,
"code": "s",
"filePath": "/Users/pony/Desktop/release/1.0.0/test/ofcms-V1.1.2/ofcms-admin/src/main/java/com/ofsoft/cms/front/controller/IndexController.java"
}, {
"desc": "",
"line": 156,
"code": "s",
"filePath": "/Users/pony/Desktop/release/1.0.0/test/ofcms-V1.1.2/ofcms-admin/src/main/java/com/ofsoft/cms/front/controller/IndexController.java"
}, {
"desc": "",
"line": 156,
"code": "s",
"filePath": "/Users/pony/Desktop/release/1.0.0/test/ofcms-V1.1.2/ofcms-admin/src/main/java/com/ofsoft/cms/front/controller/IndexController.java"
}, {
"desc": "",
"line": 159,
"code": "FrontConst.TEMPLATE_PATE + SiteUtile.getTemplatePath() + s",
"filePath": "/Users/pony/Desktop/release/1.0.0/test/ofcms-V1.1.2/ofcms-admin/src/main/java/com/ofsoft/cms/front/controller/IndexController.java"
}],
"desc": "ssti注入漏洞",
"level": "high"
}
这个路径作为漏洞存在就比较有说服力了。
这里查找出来的文件上传点是
/**
* 保存模板
*/
public void save() {
String resPath = getPara("res_path");
File pathFile = null;
if("res".equals(resPath)){
pathFile = new File(SystemUtile.getSiteTemplateResourcePath());
}else {
pathFile = new File(SystemUtile.getSiteTemplatePath());
}
String dirName = getPara("dirs");
if (dirName != null) {
pathFile = new File(pathFile, dirName);
}
String fileName = getPara("file_name");
// 没有用getPara原因是,getPara因为安全问题会过滤某些html元素。
String fileContent = getRequest().getParameter("file_content");
fileContent = fileContent.replace("<", "<").replace(">", ">");
File file = new File(pathFile, fileName);
FileUtils.writeString(file, fileContent);
rendSuccessJson();
}
很明显,这里是存在ssti漏洞的,漏洞发生的逻辑是用户上传模版文件,然后保存在服务端的模版目录下,然后在后续的渲染执行过程中,根据不同的业务场景调用不同的模版执行。这样,整体的漏洞发生的脉络就很清楚了。
4.2 逻辑漏洞分析(基于ai)
4.2.1 newbee mall 垂直越权漏洞
项目地址:https://github.com/newbee-ltd/newbee-mall
我们先来看下这个项目的权限认证逻辑是什么样的。
首先开发者使用了spring security框架对用户的请求进行鉴权,权限配置类为:
NeeBeeMallWebMvcConfigurer.java
package ltd.newbee.mall.config;
import ltd.newbee.mall.common.Constants;
import ltd.newbee.mall.interceptor.AdminLoginInterceptor;
import ltd.newbee.mall.interceptor.NewBeeMallCartNumberInterceptor;
import ltd.newbee.mall.interceptor.NewBeeMallLoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class NeeBeeMallWebMvcConfigurer implements WebMvcConfigurer {
@Autowired
private AdminLoginInterceptor adminLoginInterceptor;
@Autowired
private NewBeeMallLoginInterceptor newBeeMallLoginInterceptor;
@Autowired
private NewBeeMallCartNumberInterceptor newBeeMallCartNumberInterceptor;
public void addInterceptors(InterceptorRegistry registry) {
// 添加一个拦截器,拦截以/admin为前缀的url路径(后台登陆拦截)
registry.addInterceptor(adminLoginInterceptor)
.addPathPatterns("/admin/**")
.excludePathPatterns("/admin/login")
.excludePathPatterns("/admin/dist/**")
.excludePathPatterns("/admin/plugins/**");
// 购物车中的数量统一处理
registry.addInterceptor(newBeeMallCartNumberInterceptor)
.excludePathPatterns("/admin/**")
.excludePathPatterns("/register")
.excludePathPatterns("/login")
.excludePathPatterns("/logout");
// 商城页面登陆拦截
registry.addInterceptor(newBeeMallLoginInterceptor)
.excludePathPatterns("/admin/**")
.excludePathPatterns("/register")
.excludePathPatterns("/login")
.excludePathPatterns("/logout")
.addPathPatterns("/goods/detail/**")
.addPathPatterns("/shop-cart")
.addPathPatterns("/shop-cart/**")
.addPathPatterns("/saveOrder")
.addPathPatterns("/orders")
.addPathPatterns("/orders/**")
.addPathPatterns("/personal")
.addPathPatterns("/personal/updateInfo")
.addPathPatterns("/selectPayType")
.addPathPatterns("/payPage");
}
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/upload/**").addResourceLocations("file:" + Constants.FILE_UPLOAD_DIC);
registry.addResourceHandler("/goods-img/**").addResourceLocations("file:" + Constants.FILE_UPLOAD_DIC);
}
}
在NeeBeeMallWebMvcConfigurer类中,定义了多个拦截器,针对用户的url请求路径进行权限校验。其中/admin/开头的路由都会被转发到adminLoginInterceptor这个admin权限校验类中进行校验。下面我们来看下adminLoginInterceptor这个类的具体校验逻辑:
@Component
public class AdminLoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
String requestServletPath = request.getRequestURI();
if (requestServletPath.startsWith("/admin") && null == request.getSession().getAttribute("loginUser")) {
request.getSession().setAttribute("errorMsg", "请登陆");
response.sendRedirect(request.getContextPath() + "/admin/login");
return false;
} else {
request.getSession().removeAttribute("errorMsg");
return true;
}
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}
这里我们先简要的分析下这个代码逻辑:代码中通过request.getRequestURI获取请求的url信息,然后判断下url中是否包含/admin路由以及session中是否包含loginUser来判断其是否应该走到if的then分支中,如果if条件满足,则说明路由访问非法,需要重新登陆,反之则权限认证成功,可以执行该操作。
这里很明显是存在权限绕过缺陷的,因为我们使用的是request.getRequestURI获取的路由信息,我们知道通过request.getRequestURI获取的是用户原始的请求路由信息,用户原始的用户请求路由如果没有经过一些处理容易有url路径穿越的风险,例如我们构造:/index/..;/admin这类的路径穿越的路由即可绕过if的条件判断。
下面我们来研究下怎么规则来识别这类问题。首先,我们有一个问题,我们怎么知道这是个需要admin权限访问的路由呢?如果我们无法准确识别admin权限路由,那我们的越权分析也就无从谈起。一般来说,我们在规则层面会通过路由特征来识别,但是这个方式有点蹩脚,而且这种特征识别肯定无法非常准确的识别的。其次,我们怎么去识别条件判断逻辑是可以进行绕过的呢?
上述的admin拦截器代码很简单,只是在if逻辑中判断了下是否路由是以/admin开头。理论上来说,我们是可以写个简单规则去启发下扫描器,让它知道startswith是用于判断A字符串是否以B字符串作为起始。startswith中的字符串我们也可以获取其常量值。但是要是它不用startswith呢?用正则匹配呢?或则其他的一些判断方式呢?那我们的规则层的识别难度就很大了。那有什么办法可以比较好的处理这些问题呢?最近两年大模型逐渐火起来了,那么我们是否可以通过大模型来帮助我们处理这些涉及逻辑推理的一些问题呢?说干就干,目前Artemis已支持基于大模型的漏洞分析模块,只需要在规则编写的进行api调用即可。具体可能需要一些配置处理。主要是build.properties文件中的这几个配置项:
model_name=deepseek-ai/DeepSeek-V2.5
max_token=512
temperature=0.7
top_p=0.7
top_k=50
frequency_penalty=0.5
n=1
model_api=https://api.siliconflow.cn/v1/chat/completions
model_api是我们对接的线上大模型平台地址(我的邀请码是:RHiwUmRu),要使用大模型功能,你需要先去siliconflow这个平台注册一个帐号(新用户有2000w的免费token),然后,你需要将你的api-token作为参数传入到大模型分析模块中,具体怎么用后面会说到。model_name是我们指定的模型名称,这里我选择的是免费的deepseek-ai/DeepSeek-V2.5,当然你的预算充足也可以使用满血版的。其他参数可根据具体需求进行调整,这里不再赘述。
下面,我们用artQL来编写一个越权漏洞识别模块,来识别下这类的在拦截器中存在权限校验缺陷的问题。下面是我们的越权检测规则
<c-rule>
<id>ai-overstep-rule2</id>
<name>越权漏洞</name>
<enabled>true</enabled>
<script>
<![CDATA[
println("ai-overstep-rule2 analyze start") <br>
val knowledge:String = "/index/..;/admin这类的路由会被getRequestURI识别为/index/..;/admin,可能会绕过一些拦截器的逻辑判断,应使用getServletPath进行获取。" <br>
val system_prompt:String = "你是一个资深的代码安全分析专家,请深入分析以下代码的安全性,并给出修复方案(尽量简短)。回复内容的最后一个中文字请用于标记是否误报,若误报则标'假',反之'真'。" <br>
val md_interceptor_handler = db.method.where(_.name == "preHandle").where{ <br>
it=>{ <br>
it.params.count(_.typeName=="HttpServletRequest")>0 <br>
} <br>
} <br>
val md_interceptor_config = db.method.where(_.name == "addInterceptors").where{ <br>
it=>{ <br>
it.params.count(_.typeName == "InterceptorRegistry")>0 <br>
} <br>
} <br>
if(md_interceptor_config.nonEmpty && md_interceptor_handler.nonEmpty) { <br>
val pt = new PromptTPL() <br>
pt.user_prompt = s"请问以下代码存在越权漏洞吗?\n${md_interceptor_handler.head.code}" <br>
pt.knowledge_prompt = knowledge <br>
pt.system_prompt = system_prompt <br>
pt.token = Source.fromFile("/Users/pony/Desktop/zsd/工作资料/data/api_secrete.txt").mkString <br>
val res = pt.query.content <br>
if(res.nonEmpty) { <br>
if(res.get.endsWith("真")){ <br>
println("发现漏洞") <br>
Report.flaws.addOne(new FLAW( <br>
name = "越权漏洞", checker_name = "ai-overstep-rule1", <br>
trace = Array.empty, desc = res.get, level = "high" <br>
)) <br>
} <br>
} <br>
} <br>
println("ai-overstep-rule2 analyze end") <br>
]]>
</script>
</c-rule>
大体说下这个基于大模型的漏洞检测规则的编写思路:首先我们需要获取拦截器配置的源码,以及拦截器定义的源码,这部分操作我们可以调用artQL的模块进行获取,然后我们再添加一些prompt,prompt包括一些必要的安全知识(实测过,如果我们没有添加这些安全知识的话,大模型是无法准确的识别到这个问题的。):
/index/..;/admin这类的路由会被getRequestURI识别为/index/..;/admin,可能会绕过一些拦截器的逻辑判断,应使用getServletPath进行获取。
最后我们调用大模型分析模块PromptTPL,将所有的输入参数传入到模块中即可等待分析结果。下面是我们的最终分析报告:
输出结果如下:
{
"name": "越权漏洞",
"checker_name": "ai-overstep-rule1",
"trace": [],
"desc": "该代码存在越权漏洞。攻击者可以通过构造类似`/index/..;/admin`的请求路径绕过`startsWith(\"/admin\")`的检查,从而访问受保护的`/admin`路径。\n\n**修复方案:**\n使用`request.getServletPath()`替代`request.getRequestURI()`来获取请求路径,确保路径解析的正确性。\n\n修复后的代码如下:\n```java\n@Override\npublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {\n String requestServletPath \u003d request.getServletPath(); // 使用getServletPath获取路径\n if (requestServletPath.startsWith(\"/admin\") \u0026\u0026 null \u003d\u003d request.getSession().getAttribute(\"loginUser\")) {\n request.getSession().setAttribute(\"errorMsg\", \"请登陆\");\n response.sendRedirect(request.getContextPath() + \"/admin/login\");\n return false;\n } else {\n request.getSession().removeAttribute(\"errorMsg\");\n return true;\n }\n}\n```\n真",
"level": "high"
}
上述结果的描述信息如下:
该代码存在越权漏洞。攻击者可以通过构造类似/index/..;/admin
的请求路径绕过startsWith("/admin")
的检查,从而访问受保护的/admin
路径。
修复方案:
使用request.getServletPath()
替代request.getRequestURI()
来获取请求路径,确保路径解析的正确性。
修复后的代码如下:
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
String requestServletPath = request.getServletPath(); // 使用getServletPath获取路径
if (requestServletPath.startsWith("/admin") && null == request.getSession().getAttribute("loginUser")) {
request.getSession().setAttribute("errorMsg", "请登陆");
response.sendRedirect(request.getContextPath() + "/admin/login");
return false;
} else {
request.getSession().removeAttribute("errorMsg");
return true;
}
}
真
从结果上来看,deepseek的逻辑推理能力还是非常不错的,另外它还能够自动的给出相应的修复方案,这也是一个非常亮眼功能,当然也需要我们通过一些prompt策略去启发他。
当然,我们也可以通过一些模型训练,让大模型去掌握这些安全绕过手法,那么,我们在规则编写的时候可能就只需要给到他需要的代码块即可。
4.2.2 newbee mall 水平越权漏洞分析
我们再来分析下这个项目可能存在的一些水平越权漏洞。我们知道水平越权漏洞造成的主要原因是用户在进行一些操作的时候没有进行用户角色的权限校验。我们知道,在web项目中,绝大多数的敏感操作都是与数据库交互相关,所以越权的检测思路,我们不妨围绕是否越权访问数据库操作api来进行分析。我们通过源码分析不难发现,newbee这个项目的数据库操作主要是基于ibatis api,那么我们不妨将ibatis的一些数据操作api设置为sink点,然后向上回溯分析是否进行了权限校验。但是,我们如何知道哪里进行了权限校验呢?怎么进行的权限校验呢?这可能需要规则编写的工程师有丰富的研发经验,这样才能够对各种水平鉴权机制了然于胸,那么有没有一种方式,能够让我们在不知道项目中是怎么水平鉴权的情况下,还能够识别出潜在的水平鉴权的问题呢?那我们还是将这个问题丢给大模型吧。这里具体的做法是,我们可以将web入口到sql操作api之间的调用链涉及的代码喂给大模型,然后给出适当的prompt让大模型结合我们给到的上下文信息去判断当前这个数据流下这个sql操作是否存在水平越权风险。规则如下:
<c-rule>
<id>ai-overstep-rule1</id>
<name>越权漏洞检测</name>
<enabled>true</enabled>
<script>
<![CDATA[
val system_prompt:String = "你是一个资深的代码安全分析专家,请深入分析以下代码的安全性,并给出修复方案(尽量简短)。回复的最后一个字请用于标记是否误报,若误报则标'假',反之标'真'。" <br>
val sinks=db.call.where{ <br>
it=>{ <br>
it.name.contains("deleteByPrimaryKey") || it.name.contains("selectByPrimaryKey") <br>
} <br>
} <br>
val md_list:ListBuffer[MethodDecl]=new ListBuffer[MethodDecl] <br>
sinks.foreach{ <br>
it=>{ <br>
if(it.method!=null && !md_list.contains(it.method)){ <br>
md_list.addOne(it.method) <br>
} <br>
} <br>
} <br>
//对所有method进行调用链分析 <br>
val cg = new CallGraphAnalyze() <br>
val chains=md_list.flatMap{ <br>
it=>{ <br>
cg.fetch_call_chains(method = it) <br>
} <br>
} <br>
if(chains.nonEmpty){ <br>
var code_snippet:String=chains.last.methods.map(_.code).mkString("\n") <br>
val pt = new PromptTPL() <br>
pt.user_prompt = s"请问以下代码存在越权漏洞吗?\n${code_snippet}" <br>
pt.knowledge_prompt = "" <br>
pt.system_prompt = system_prompt <br>
pt.token = Source.fromFile("/Users/pony/Desktop/zsd/工作资料/data/api_secrete.txt").mkString <br>
val res = pt.query.content <br>
if (res.nonEmpty) { <br>
if (res.get.endsWith("真")) { <br>
println("发现漏洞") <br>
println(res.get) <br>
Report.flaws.addOne(new FLAW( <br>
name = "越权漏洞", checker_name = "ai-overstep-rule1", <br>
trace = Array.empty, desc = res.get, level = "high" <br>
)) <br>
} <br>
} <br>
} <br>
]]>
</script>
</c-rule>
以下是检出报告:
{
"name": "越权漏洞",
"checker_name": "ai-overstep-rule1",
"trace": [],
"desc": "代码存在越权漏洞。`getOrderItems`方法直接根据传入的`id`查询订单信息,但没有验证当前用户是否有权限访问该订单。攻击者可以通过构造恶意请求,获取其他用户的订单信息。\n\n**修复方案:**\n在`getOrderItems`方法中添加用户权限验证,确保查询的订单属于当前登录用户。\n\n```java\n@Override\npublic List\u003cNewBeeMallOrderItemVO\u003e getOrderItems(Long id, Long userId) {\n NewBeeMallOrder newBeeMallOrder \u003d newBeeMallOrderMapper.selectByPrimaryKey(id);\n if (newBeeMallOrder !\u003d null \u0026\u0026 newBeeMallOrder.getUserId().equals(userId)) { // 验证订单是否属于当前用户\n List\u003cNewBeeMallOrderItem\u003e orderItems \u003d newBeeMallOrderItemMapper.selectByOrderId(newBeeMallOrder.getOrderId());\n if (!CollectionUtils.isEmpty(orderItems)) {\n List\u003cNewBeeMallOrderItemVO\u003e newBeeMallOrderItemVOS \u003d BeanUtil.copyList(orderItems, NewBeeMallOrderItemVO.class);\n return newBeeMallOrderItemVOS;\n }\n }\n return null;\n}\n```\n\n在控制器中调用`getOrderItems`时,传入当前登录用户的`userId`:\n\n```java\n@GetMapping(\"/order-items/{id}\")\n@ResponseBody\npublic Result info(@PathVariable(\"id\") Long id, @RequestAttribute(\"userId\") Long userId) { // 获取当前登录用户的userId\n List\u003cNewBeeMallOrderItemVO\u003e orderItems \u003d newBeeMallOrderService.getOrderItems(id, userId);\n if (!CollectionUtils.isEmpty(orderItems)) {\n return ResultGenerator.genSuccessResult(orderItems);\n }\n return ResultGenerator.genFailResult(ServiceResultEnum.DATA_NOT_EXIST.getResult());\n}真",
"level": "high"
}
desc内容友好展示如下:
代码存在越权漏洞。`getOrderItems`方法直接根据传入的`id`查询订单信息,但没有验证当前用户是否有权限访问该订单。攻击者可以通过构造恶意请求,获取其他用户的订单信息。
**修复方案:**
在`getOrderItems`方法中添加用户权限验证,确保查询的订单属于当前登录用户。
@Override
public List<NewBeeMallOrderItemVO> getOrderItems(Long id, Long userId) {
NewBeeMallOrder newBeeMallOrder = newBeeMallOrderMapper.selectByPrimaryKey(id);
if (newBeeMallOrder != null && newBeeMallOrder.getUserId().equals(userId)) { // 验证订单是否属于当前用户
List<NewBeeMallOrderItem> orderItems = newBeeMallOrderItemMapper.selectByOrderId(newBeeMallOrder.getOrderId());
if (!CollectionUtils.isEmpty(orderItems)) {
List<NewBeeMallOrderItemVO> newBeeMallOrderItemVOS = BeanUtil.copyList(orderItems, NewBeeMallOrderItemVO.class);
return newBeeMallOrderItemVOS;
}
}
return null;
}
在控制器中调用`getOrderItems`时,传入当前登录用户的`userId`:
@GetMapping("/order-items/{id}")
@ResponseBody
public Result info(@PathVariable("id") Long id, @RequestAttribute("userId") Long userId) { // 获取当前登录用户的userId
List<NewBeeMallOrderItemVO> orderItems = newBeeMallOrderService.getOrderItems(id, userId);
if (!CollectionUtils.isEmpty(orderItems)) {
return ResultGenerator.genSuccessResult(orderItems);
}
return ResultGenerator.genFailResult(ServiceResultEnum.DATA_NOT_EXIST.getResult());
}真
上述问题对应的漏洞源码是:
/**
* 详情
*/
@GetMapping("/order-items/{id}")
@ResponseBody
public Result info(@PathVariable("id") Long id) {
List<NewBeeMallOrderItemVO> orderItems = newBeeMallOrderService.getOrderItems(id);
if (!CollectionUtils.isEmpty(orderItems)) {
return ResultGenerator.genSuccessResult(orderItems);
}
return ResultGenerator.genFailResult(ServiceResultEnum.DATA_NOT_EXIST.getResult());
}
@Override
public List<NewBeeMallOrderItemVO> getOrderItems(Long id) {
NewBeeMallOrder newBeeMallOrder = newBeeMallOrderMapper.selectByPrimaryKey(id);
if (newBeeMallOrder != null) {
List<NewBeeMallOrderItem> orderItems = newBeeMallOrderItemMapper.selectByOrderId(newBeeMallOrder.getOrderId());
//获取订单项数据
if (!CollectionUtils.isEmpty(orderItems)) {
List<NewBeeMallOrderItemVO> newBeeMallOrderItemVOS = BeanUtil.copyList(orderItems, NewBeeMallOrderItemVO.class);
return newBeeMallOrderItemVOS;
}
}
return null;
}
我们大概分析下源码,很明显,这里是存在水平越权漏洞的。因为其在进行sql操作newBeeMallOrderMapper.selectByPrimaryKey(id)的时候没有进行用户权限的判定。说明大模型的推断还是比较靠谱的。而这里大模型给到的修复方案是从数据库查询出来的userid和用户给的userid进行比较,这个方法也ok。当然他这个系统中其他逻辑代码中进行水平鉴权主要是从session中获取userid,这个也是一种办法。
五、使用Artemis进行mr扫描
5.1 什么是mr扫描以及为什么要进行mr扫描
mr扫描,即merge request扫描,指在进行分支合并的时候进行代码安全扫描。一般来说,在这个环节中进行扫描,我们期望是能够识别出项目的一些增量漏洞。另外,我们也希望在这个环节中的扫描能够更快速一些,这样不至于卡住我们的代码合并,毕竟有时候想要上线某个功能的心情是很迫切的,但是被扫描时间卡住,则是一种非常不好的体验。
现在,很多团队在增量漏洞识别方面都能够有比较不错的实践,例如基于一些漏洞管理平台针对不同代码版本进行的new code计算得出的增量漏洞识别方法,但是,在代码扫描层面实现增量却没有发现太多有相关的落地实践。探究其原因呢,我认为有以下几个主要原因,第一个是有些商业扫描器是支持mr扫描的,但是由于官方的技术帮助文档比较粗糙,以及中国区支持人员针对此类实践的支持不够到位,使得很多公司的sdl工程师都很难落地实践mr扫描;第二个是一些错误的言论在网上误导一些sdl工程师,使其认为mr扫描是一种缺乏准确性的扫描实践;第三个是购买的代码扫描器本身不支持mr扫描,例如诸多国产的代码安全扫描产品。
为了能够解决这一难题,Artemis静态代码安全扫描器在设计之初就考虑到了这点,所以从架构方面我就努力朝着能够更加容易进行mr扫描方向设计。下面,我为大家讲解下Artemis的mr扫描的原理以及具体的实际应用。
要讲清楚 Artemis mr扫描的原理,首先,我说明下,artemis在编译时捕获的是什么东西,以及产出物是什么。
artemis在编译时会以源码文件为单元,进行类型信息、ast结构信息、控制流信息、数据流指向信息捕获,然后将这些捕获的信息持久化存储,作为artemis的编译产出物。此时,我们捕获的语义信息不依赖外部的信息,这也是我们代码语义信息增量捕获得以实现的基本前提。
那么artemis是如何进行增量语义信息捕获的呢?首先,artemis会先对全量的代码信息进行hash记录,保存在source.txt中,然后我们在进行增量捕获的时候,会通过这个文件进行hash比对,从而计算出需要进行编译捕获的文件。我们计算出有增量修改的文件之后,我们就不必所有代码文件都进行捕获了,只需要捕获这些文件即可,而且我们的artemis扫描器的语义信息不依赖外部的文件,所以这里不存在什么语义信息损失的说法,我们只需要将原来捕获的语义信息拷贝过来,将增量的文件进行语义信息捕获,并替换掉原来对应的语义文件即可。
这一过程,我们可以通过以下的示意图进行表示:
5.2 使用artemis进行mr扫描
我们以一个case为例演示下如何使用artemis进行增量代码扫描。
我们以owaps的这个benchmark文件为例:https://github.com/OWASP-Benchmark/BenchmarkJava
这个benchmark大概有2730个文件,如果全量捕获的话,是非常耗时的。我们先使用artemis对其进行全量代码语义信息捕获,看看其耗时如何?
执行以下命令接口进行全量代码捕获
java -Xss100m -Xms3096m -Xmx8044m -jar Artemis.jar -p /Users/pony/Desktop/release/1.0.0/test/tt/BenchmarkJava -od workspace
可以看到,artemis捕获全量源码文件大概耗时:1074s,约合18分钟。
下面我们来进行增量代码捕获
我们随便修改下一个文件的代码,使其hash改变。然后我们使用artemis对这个项目进行捕获。
命令
java -Xss100m -Xms3096m -Xmx8044m -jar Artemis.jar -p /Users/pony/Desktop/release/1.0.0/test/BenchmarkJava-master -od workspace
可以发现,使用增量捕获之后,捕获时间仅仅花了75s,比全量捕获整整省了93.1%的时间,这显然是一个非常喜人的结果,尤其是对于一些需要在流水线上进行卡点且对扫描准确性和覆盖率要求比较高的项目,这无疑是一个福音。
六、静态代码分析技术的展望
随着大模型的发展,静态代码也将迎来新的契机。之前一些使用传统手段无法很好解决的问题,比如误报判定,更加专业性及针对性的代码修复建议,自动化代码修复,逻辑漏洞的分析等等,都将因为大模型的引入而有了新的解决思路。而有了大模型的引入,一些老牌的静态代码厂商形成的技术壁垒可能会被一系列基于大模型的创新的检测策略打破,一些致力于更深层次代码分析的实践者,抓住机遇,或许能够在这个领域形成新的竞争力。
七、程序下载地址
使用说明
pony@ponydeMBP 1.0.0 % java -jar Artemis.jar -h
Usage: SAST [-hV] [-bc=<build_cmd>] [-d=<dbpath>] [-j=<java_home>]
[-mr=<mr_enable>] [-od=<out_dbpath>] [-p=<path>] [-r=<rulePath>]
[-t=<mr_target_path>]
static code analysis.
-bc, --build-command=<build_cmd>
指定编译命令.
-d, --database=<dbpath> 指定加载的数据库路径.
-h, --help Show this help message and exit.
-j, --java-home=<java_home>
指定java_home路径.
-mr, --mr-enable=<mr_enable>
是否启用mr扫描.
-od, --output-database=<out_dbpath>
指定输出的编译数据库路径.
-p, --path=<path> 指定源码路径.
-r, --rule=<rulePath> 指定规则路径.
-t, --mr-target-path=<mr_target_path>
指定target数据库路径.
-V, --version Print version information and exit.
4A评测 - 免责申明
本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。
不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。
本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。
如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!
程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。
侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)