《网安面试指南》http://mp.weixin.qq.com/s?__biz=MzkwNjY1Mzc0Nw==&mid=2247484339&idx=1&sn=356300f169de74e7a778b04bfbbbd0ab&chksm=c0e47aeff793f3f9a5f7abcfa57695e8944e52bca2de2c7a3eb1aecb3c1e6b9cb6abe509d51f&scene=21#wechat_redirect
《Java代码审计》http://mp.weixin.qq.com/s?__biz=MzkwNjY1Mzc0Nw==&mid=2247484219&idx=1&sn=73564e316a4c9794019f15dd6b3ba9f6&chksm=c0e47a67f793f371e9f6a4fbc06e7929cb1480b7320fae34c32563307df3a28aca49d1a4addd&scene=21#wechat_redirect
《Web安全》http://mp.weixin.qq.com/s?__biz=MzkwNjY1Mzc0Nw==&mid=2247484238&idx=1&sn=ca66551c31e37b8d726f151265fc9211&chksm=c0e47a12f793f3049fefde6e9ebe9ec4e2c7626b8594511bd314783719c216bd9929962a71e6&scene=21#wechat_redirect
SQL注入
审计的注意点
是否使用预编译技术,预编译是否完整。定位 SQL 语句上下文,查看是否有参数直接拼接,是否有对模糊查询关键字的过滤。Mybatis 框架则搜索 ${},四种情况无法预编译:like 模糊查询、order by 排序、范围查询 in、动态表名/列名,只能拼接,所以还是需要手工防注入,此时可查看相关逻辑是否正确。
关键函数或字符串查找
-
Statement
-
createStatement
-
PrepareStatement
-
like '%${
-
in(${
-
in (${
-
select
-
update
-
insert
-
delete
-
${
-
setObject(
-
setInt(
-
setString(
-
setSQLXML(
-
createQuery(
-
createSQLQuery(
-
createNativeQuery(
JDBC
Statement
SQL语句动态拼接导致的SQL注入漏洞是先前最为常见的场景,createStatement() :创建一个 Statement 对象,之后可使用 executeQuery() 方法执行SQL语句。executeQuery(String sql) 方法:执行指定的 SQL 语句,返回单个 ResultSet 对象。
根据案例可以看到用户可以传入参数id,id未经过任何处理直接拼接到SQL语句里,然后使用executeQuery()执行了SQL语句。运行代码进行调试测试。
测试存在SQL注入漏洞。
PrepareStatement
预编译是能够有效防止注入的,但是存在预编译使用错误的情况,最后导致SQL注入的产生。
在上述案例中就是虽然使用了preparestatement预编译,但是未对参数进行标记而导致了SQL注入产生。启动代码调试
预编译正确写法
可以看到这次无法注入了。
Order by注入
在SQL语句中, order by 语句用于对结果集进行排序。而order by 语句后面需要是字段名或者字段位置,无论是preparestatement还是mybatis中,都无法直接预编译,因为会将传入的参数用单引号包裹,从而认为传入的是字符串而不是字段名,因此在使用order by时,无法使用预编译进行防止注入。
启动代码,使用sqlmap直接跑,可以看到存在注入。
审计实战
源码地址: https://gitee.com/getrebuild/rebuild/tree/3.5.4/
搭建好系统后可以再pom.xml里看使用了什么框架,不存在mybatis框架就直接在代码里查看是否存在未预编译得代码。
第一步首先获取用户传入的feeds参数,这里的 "feedsId = '%s'" 是一个模板字符串,其中的%s是一个占位符,表示字符串类型的数据。feeds 是一个变量,其值将被插入到模板字符串中的%s位置。第四步再将sqlwhere拼接进SQL语句,第五步执行SQL语句。所以该参数不存在预编译。只用了getIdParameterNotNull方法进行处理。
跟进该方法,可以看到使用getParameter方法获取传入的参数,然后再用isid校验了参数v,跟进参数v
第一个if需要实例一模一样,无法传入我们的语句,所以直接看第二个,可以看到需要满足上述三个条件才会进入下一步校验,分别是校验了是否是null,如果转换成字符串后是否是空,第三个是校验了传入的参数的长度。跟进长度要求.
可以看到上述为静态初始化块,静态初始化块(static 块)在 Java 程序中会在类被加载时自动执行。具体来说,静态初始化块有以下几个特点:自动执行:当类首次被加载时,静态初始化块会自动执行。执行时机:在类的任何成员(包括静态成员)被访问之前执行。所以当前的idLength取决于idGenerator类的getlenth,而idGenerator是通过反射获取的WeakIDGenerator,所以这里getlenth来源于WeakIDGenerator类,追进去查看。
可以看到长度为20。返回刚才的代码
验证完成后表达式 id.toString().charAt(3) == '-' 用于检查变量 id 转换为字符串后,第四个字符(索引为 3)是否为-,所以得出结果,我们需要传入的参数首先是20位,而且第四位参数必须为-,所以构造参数为000-0000000000000000
可以看到调用成功。添加payload 001-00000000%27or+1=1%23发现报错。由于长度限制未进行后续测试,主要是学习审计思路。
Mybatis
在mybatis中和#的区别就是一个有预编译一个没有,其中是未进行预编译直接进入数据库查询的,所以需要查看当前参数是否为用户所控制,如果是用户可控的情况下,那么说明存在SQL注入。
常规$注入
源码地址: https://gitee.com/mingSoft/MCMS/tree/5.2.8/
参数为用户所控制的情况下,未对传入的参数进行过滤就会造成SQL注入,接下来用代码举例说明。
可以看到直接使用了$,我们需要从下往上追,首先我们需要找到其对应的映射器,其位置一般位于最上方。
我们需要进入这个dao文件搜索出现存在$的id,就比如上述出现queryChildren。
其中也可能会存在在当前dao文件里未找到该id的情况,这个时候我们需要查看他的父类,也就是上述图片中的IBasedao文件。那么我们找到这个id后就需要追踪谁调用了这个方法,直接Ctrl+鼠标左键追踪即可查看到。
继续往上追,可以看到存在四个调用,这个时候就需要判断哪个调用用户可控且不存在过滤。
这里我直接进入存在注入的点进行分析,可以看到传入的参数为categoryEntity,继续追categoryEntity来源。
根据图上可以看到,categoryEntity来源于categoryid,而categoryid来源于目录{categoryId},再分析代码,只判断了categoryId是否为0,那么整个流程已经理清。
所以构造URL进行注入。
注入成功。
Order by注入
与JDBC预编译中order by注入一样,在 order by 语句后面需要是字段名或者字段位置。因此也不能使用Mybatis中预编译的方式。如果我们要防止注入,只能限制用户传入的数据使我们想要的数据,使用白名单方式即可,下面两种修复方式均可。
In注入
in在查询某个范围数据是会用到多个参数,在Mybtis中如果直接使用占位符 #{} 进行查询会 将这些参数看一个整体,进而引发报错,所以开发人员可能会直接使用$,从而导致了SQL注入的产生,而我们正确的做法应该是foreach配合占位符 #{} 实现IN查询。这样就不会导致注入的产生。
Like注入
Like语句在查询时直接使用#{}会报错,如果开发人员经验不太充足的情况下可能会直接使用${}进行查询,从而导致SQL注入的产生。正确做法应该是like CONCAT(CONCAT('%',#{item.value}),'%')。
命令执行
审计的注意点
命令执行漏洞是指应用有时需要调用一些执行系统命令的函数,如果系统命令代码未对用户可控参数做过滤,则当用户能控制这些函数中的参数时,就可以将恶意系统命令拼接到正常命令中,从而造成命令执行攻击。首先我们了解一下命令连接符的使用。
Windows与Linux均支持:
cmd1 | cmd2 只输出cmd2的结果,但两个命令均会执行
cmd1 || cmd2 只有当cmd1执行失败后,cmd2才被执行
cmd1 & cmd2 先执行cmd1,不管是否成功,都会执行cmd2
cmd1 && cmd2 先执行cmd1,cmd1执行成功后才执行cmd2,否则不执行cmd2
Linux单独支持:
cmd1;cmd2 依次执行命令
需关注的函数:
-
Runtime.getRuntime().exec()
-
Process
-
UNIXProcess
-
ProcessImpl
-
ProcessBuilder.start()
-
GroovyShell.evaluate()
Runtime.getRuntime().exec()
java.lang.Runtime 公共类中的 exec()方法可以执行系统命令,其中需要关注是否为cmd /c,在不使用cmd /c的时候,只能执行单个命令,无法使用|和&进行连接执行多个命令,当使用|和&连接时会将其当成一个整体命令执行。而使用cmd /c就可以执行复杂命令。
上述代码中,userinput模拟用户输入,command为执行的命令,尝试执行ipconfig && dir。
执行成功。但是在审计的时候除了正射调用,也可能存在反射调用exec进行命令执行。
上述代码就展示了反射如何调用java.lang.Runtime 公共类中的 exec()方法,尝试进行命令执行。
探索一下如何调用的,首先进入exec方法。
再次进入下面return的方法。
可以看到最终调用的是ProcessBuilder方法,那么下一个就是processbuilder的命令执行。
ProcessBuilder.start()
ProcessBuilder.start() 方法的主要作用就是启动一个新的操作系统进程。这个进程独立于当前的 Java 应用程序,有自己的输入、输出和错误流。通过这个方法,你可以执行外部命令或脚本,就像在命令行中手动执行一样。需要注意的是,在processbuilder中传入的cmd只能以字符串数组的形式来传递命令和参数,否则会出现报错。
测试命令执行。
执行成功。我们再跟进一下processbuilder.start()查看其调用,跟进start,可以看到最终执行是ProcessImpl.start()这里ProcessImpl更为底层。Runtime和ProcessBuilder执行命令实际上也是调用了ProcessImpl这个类。
ProcessImpl.start()
ProcessImpl 类实际上是 Java 运行时环境内部用于实现 Process 接口的具体实现类。它通常是私有的,并且不是公共 API 的一部分,因此开发者通常不能直接实例化或调用 ProcessImpl 中的方法,所以一般调用此方法需要通过反射的形式进行调用。
代码示例如图所示,如果未经过过滤则会直接导致命令执行。
审计实战
源码地址: https://github.com/xuxueli/xxl-job
搭建好后,分别启动调度中心和执行器。搭建好系统后进行测试,审计。了解到存在命令执行后进行测试,存在的点是后台任务管理执行脚本处,点击保存。
保存完后,修改GLUE IDE内容。
添加whoami,保存执行。
查看执行日志。
命令执行成功,跟踪代码查看。首先可以知道得是入口为/trigger,查看调用。
跟进addTrigger方法。
这里得代码主要是使用选定的 triggerPool_ 线程池来执行一个 Runnable 实例中的任务。尝试调用 XxlJobTrigger.trigger() 方法来触发任务,该方法包含了实际的任务处理逻辑,并允许传入任务ID (jobId)、触发类型 (triggerType)、失败重试次数 (failRetryCount)、执行分片参数 (executorShardingParam) 和执行参数 (executorParam)。跟进XxlJobTrigger.trigger()。
跟进后发现我们传入的参数executorParam添加进了jobinfo。
最后经过一系列判断后jobinfo被传入进了processTrigger方法里进行处理。
跟进processTrigger方法查看如何处理jobinfo,可以看到jobinfo传入进来之后,将其中得数据去除添加进triggerparam中,可以看到一个getgluesource,该参数就是我们上述传入得GLUE IDE内容。Debug查看一下。
继续跟进triggerParam的处理,可以看到有两个处理了triggerParam,其中第一个是根据指定的路由策略从一组注册的执行器中选择一个合适的执行器地址,以便将任务分发给该执行器进行执行。那么我们跟进第二个方法。
跟进runExecutor方法查看如何处理triggerParam,可以看到首先根据address获取执行器,这里获取执行器(9999端口启动的),并委托ExecutorBiz执行run方法。
跟进run方法,可以看到有一个实现,继续跟进这个实现
在该方法里看到,先是判断glueTypeEnum是否为BEAN,或者为GLUE_GROOVY,可以得知此处校验的是我们最初传入进来的powershell。
所以代码未走上述的两个条件,可以看到jobhandler为null,if判断是否为null,是的情况下创建了一个新的实例ScriptJobHandler。
再继续往下走由上述代码中可以看到jobThread = null;那么就应该走下面为null的代码,可以看到调用了registJobThread处理上述实例化的ScriptJobHandler。
继续跟进registJobThread方法,可以看到创建了一个JobThread 实例,通过调用 start() 方法启动了新的 JobThread 实例。这实际上会创建一个新的线程,并在该线程中执行 JobThread 的 run() 方法。
那么跟进JobThread 的run方法,实例中传入了handler,追踪handler参数,可以看到代码中使用if判断,发现无论走上面的if还是下面的else都会调用handler.execute去处理triggerParam.getExecutorParams()。
跟进execute方法,可以看到需要找具体的handler实现,而当初我们传进去的handler为ScriptJobHandler,那么直接追进ScriptJobHandler的execute方法中。
跟进ScriptJobHandler类当中,可以看到,获取了几个参数。
其中cmd为gluetype中获取的,根据我们最开始选择的powershell可得知,此处cmd为powershell。
这段代码的作用是构建一个脚本文件的路径,并确保该文件存在。如果文件不存在,则创建该文件并写入脚本内容,写入的内容也是我们传进来的gluesource。
继续往下看scriptParams来源于上述我们传入的params,最后再传入最下面的execToFile进行处理。
跟进execToFile发现我们传入的sciptfile被添加进了cmdarry中。
除了上述两个参数被添加近cmdarray当中,还将传入的params数组中的每个元素添加到一个集合 cmdarray 中。将List类型的集合转换为一个 String[] 类型的数组后直接执行数组里的命令,至此整个利用链已经理清,上述复现只复现了gluesource中造成的命令执行,其中还有我们传入的executorParam造成的命令执行,可以自己尝试着复现一下。
任意文件操作
审计的注意点
常见的一些java文件操作类的漏洞:任意文件的读取、下载、删除、修改,这类漏洞的成因基本相同,都是因为程序没有对文件和目录的权限进行严格控制,或者说程序没有验证请求的资源文件是否合法导致的。
需关注的函数
-
sun.nio.ch.FileChannelImpl
-
java.io.File.list/listFiles
-
java.io.FileInputStream
-
java.io.FileOutputStream
-
java.io.FileSystem/Win32FileSystem/WinNTFileSystem/UnixFileSystem
-
sun.nio.fs.UnixFileSystemProvider/WindowsFileSystemProvider
-
java.io.RandomAccessFile
-
sun.nio.fs.CopyFile
-
sun.nio.fs.UnixChannelFactory
-
sun.nio.fs.WindowsChannelFactory
-
java.nio.channels.AsynchronousFileChannel
-
FileUtil/IOUtil
-
filePath/download/deleteFile/move/getFile
-
fileName/filePath
文件读取
对传入的路径未做严格的校验,导致攻击者可以自定义路径,注意操作文件是否存在过滤../和..\等相关操作。使用代码进行测试。
可以看到上述代码未经过过滤直接创建并返回了相应的文件资源,启动环境尝试一下。
可以看到读取成功。
文件删除
审计方式也是同理主要是查看是否存在过滤相关的。
启动环境测试。
文件上传
源码地址: https://gitee.com/mingSoft/MCMS/tree/5.2.6/
文件上传注意的点是需要查看代码对于上传是否有限制,如果有限制需要查看存在什么限制,是校验的什么,然后再针对进行绕过测试。那么我们开始审计。
首先看代码层面只校验了路径中是否包含../和..\主要是为了防止路径穿越。
再去看配置文件可以发现,全局存在黑名单校验,不允许上传如图所示的后缀。
从依赖中看到使用了模板引擎freemarker。
在Freemarker如果使用了自定义标签,并且这些标签在模板中被调用时没有正确过滤输入,也可能导致执行系统命令。可以覆盖存在的ftl文件来执行我们想执行的命令。
可以看到图上存在一个五个参数,其中null可以不传入,其他四个分别为上传路径,上传文件的名称,是否使用上传文件夹路径,是否重命名。先确定我们要上传的路径。
尝试覆盖上述的index.ftl,那么构造我们的payload,(<#assign ex="freemarker.template.utility.Execute"?new()>${ex("whoami")}),上传测试。
再去找我们对应的index.ftl文件的页面。
访问http://127.0.0.1:8080/ms/cms/category/index,命令执行成功。
审计实战
源码地址:https://gitee.com/mjtop/JTopCMSV3/releases/tag/JTopCMSV3.0.2-OP
搭建完系统直接开始审计代码,全局搜索/download相关的信息。
存在两个下载接口,这两个接口均存在任意文件下载读取,我们追踪第二个进行审计。可以看到主要文件参数来源于我们传入的target。
而我们的target来源于上面的params, params的来源于处理完的request请求
查看如何处理的当前请求,跟进ServletUtil.getRequestInfo查看如何处理的。
继续跟进可以看到有一段代码处理了request,Enumeration enumeration = request.getParameterNames();Enumeration是一个枚举类,在这里用来列举出所有的参数名称。你可以通过这个Enumeration来遍历所有的参数名,并且对于每一个参数名,你可以使用request.getParameter(String paramname)方法来获取对应的值。
依次走下来,发现会在红框处处理我们的参数,判断decodeMode是true还是false,而decodeMode来源于上述直接传入的false,因此是走下面SystemSafeCharUtil.decodeDangerChar
跟进查看decodeDangerChar方法,就是将我们传入的**!1**这类型的转成正常的字符。
所以在这里的代码对于用户传入的target只有这上面一层处理,未发现处理../相关的方法,尝试构造../1.txt获取我们编辑的文本内容,根据最开始的路径可以得出,功能点在备份处,下载备份的功能。
下载抓包测试。
发送到repeater进行测试,发现未成功读取,既然当前代码层面不存在防御,那么查看是否存在过滤器。
在web.xml中找到了过滤器,一共存在两个与之对应的过滤器,其中securitySessionDispose的过滤器是对应的是鉴权过滤器。
直接查看另一个过滤器,这个过滤器内容实在是太多,而且开发的命名也是极其抽象,无法阅读下去,只能搜索我们的../相关的信息,可以看到存在静态代码块包含了危险字符,猜测为过滤的内容。
分析$_6的调用,可以看到被当作参数传入了$_1
跟进_$1,但是由于代码写的很复杂,大概就是匹配了危险字符,并打印出来,与我们的打印台的错误对应上了
打印台报错内容为..
所以在此是被过滤器拦截了,但是,在我们之前审计过程中遇见了一个字符转换的处理功能,那么我们是否可以使用他这种处理方式来读取我们想要读取的内容呢?
构造测试,在上一级目录放置了一个1.txt文本,可以看到顺利读取成功。
查看代码获取到的我们的target参数,第一步初始参数为我们正常的传入的值。
第二部最后获取到的target参数。
审计完毕,读取到我们想要的内容。
SSRF
审计的注意点
SSRF 漏洞出现的场景有很多,如在线翻译、转码服务、图片收藏/下载、信息采集、邮件系统或者从远程服务器请求资源等。通常我们可 以通过浏览器查看源代码查找是否在本地进行了请求,也可以使用 DNSLog 等工具进行测试网页是否被访问。
重点关注 HTTP 请求操作函数。想要支持所有的协议,只能使用 URLConnection、URL
。
需关注的函数
-
HttpClient.execute()
-
HttpClient.executeMethod()
-
HttpURLConnection.connect()
-
HttpURLConnection.getInputStream()
-
URL.openStream()
-
HttpServletRequest()
-
BasicHttpEntityEnclosingRequest()
-
DefaultBHttpClientConnection()
-
BasicHttpRequest()
-
ImageIO.read()
-
Request.Get.execute
-
Request.Post.execute
-
OkHttpClient.newCall.execute
-
com.alibaba.druid.util.HttpClientUtils
-
javax.servlet.http.HttpServletRequest
-
java.net.URI
-
java.net.URL
-
java.net.URLConnection
-
com.bea.uddiexplorer.Search
-
org.apache.commons.httpclient.HttpMethodBase
-
org.apache.http.client.methods.HttpRequestBase
除了建立 HTTP 协议连接,还可能直接通过 Socket 建立连接
-
AsynchronousServerSocketChannel.accept/bind
-
AsynchronousSocketChannel.write/read/bind/connect
-
ServerSocketChannel.bind
-
ServerSocket.accept/bind
-
Socket.bind/connect
-
Socket.getInputStream().read
-
Socket.getOutputStream().write
-
SocketChannel.bind/read/write/connect
Execute()
在Java中,execute()方法常常在HttpClient ,HttpAsyncClient ,okhttp3
,Hutool库中使用,它负责发送HTTP请求并接收响应。审计execute()调用时,需要检查用于构造请求的参数(如URL)是否经过了充分的验证和过滤。如果execute()的参数可以直接被用户控制,而没有适当的输入验证,这可能表明存在SSRF风险。
在这个案例中,使用了execute执行了http请求,其中传入的URL为用户可控,而代码层面未对用户传入的URL参数进行过滤,导致了漏洞的产生。
在网页上尝试复现该漏洞
可以看到这个代码同样存在相同问题,URL未经过过滤直接传入,直接execute执行。
openConnection()
openConnection()是建立网络请求的第一步,它创建了一个到目标URL的网络连接。在SSRF审计中,审计人员会关注这里是否使用了用户提供的或不受信任的输入来构造URL,因为这可能允许攻击者控制服务器发起的请求。
在这个例子中URL未经过过滤,openConnection() 方法直接执行了HTTP请求,导致了ssrf漏洞的产生。启动程序查看结果
openstream()
openStream()是java.net.URL类中的一个方法,用于打开到由URL表示的资源的连接,并返回一个InputStream对象,该对象可以用来读取资源的内容。当调用URL对象的openStream()方法时,实际上会创建一个与指定URL对应的网络连接,并返回一个InputStream,这样你就可以通过这个流读取URL所引用的资源。这个过程相当于调用了URL对象的openConnection()方法,然后从返回的URLConnection对象中获取输入流。
在这个案例中就是使用了openstream方法直接访问了未经过滤的URL导致了ssrf漏洞的产生。
Socket
Socket类提供了基本的TCP/IP网络通信功能,允许程序创建客户端Socket连接到远程服务器,或者作为服务器接收来自客户端的连接。在审计代码中,Socket相关的方法调用通常会揭示应用程序如何与远程服务进行交互,包括发起HTTP请求、DNS查询等。
这个案例就是通过socket请求的相关资源,在Java中,使用Socket类来直接请求URL并不常见,一般会使用上述案例中的HttpURLConnection,HttpClient。我们启动一下代码查看结果