Access Server is a great tool that you can install on a server with just a couple of commands. It makes client management easy—edit subnets, profiles, passwords, and more. Doing all of that manually would be much more cumbersome.
Here’s the catch: the free version limits the number of connections—only two clients can be active at the same time. The paid license is pricey, and you can’t pay for it with a Russian credit card right now anyway. Unfortunately, the same goes for many apps and useful hacker resources. Just yesterday, for example, I couldn’t pay for Hack The Box, which really bummed me out. So instead of tackling HTB boxes, we’ll practice the old Russian pastime—cracking!
warning
As of this writing, the laws prohibiting the use of unlicensed software remain in force. The idea of allowing unlicensed use of software from companies exiting Russia is only being discussed. We are merely demonstrating a vulnerability that enables bypassing license verification. If you choose to apply this knowledge, you do so at your own risk. Keep track of current legislation, especially if you’re operating commercially.
Rather than reinvent the wheel, let’s see what the search engines return for the query “OpenVPN Access Server license unlimited.” Interestingly, there’s a repository that was removed from GitHub for copyright infringement. We find a mirror and check the description: it requires CentOS 7. In the installation script, after installing the openvpn-as package itself, it also replaces the file pyovpn-2. at /.
I wouldn’t replace such a critical system component—especially one that handles the web frontend—sight unseen, and I don’t recommend you do either. All the more so with privacy-focused software. So before we use it, let’s inspect what’s inside. We’ll compare the hacked builds against the original files.
We’ll unpack the original RPM to find pyovpn-2.. You can extract a .rpm with plain tar:
$ tar xf openvs.rpm
A .egg file is just a standard ZIP archive. You’ll find it at pyovpn-2..
Next, unpack the hacked pyovpn-2.0-py2.7.egg from the bundle:
$ unzip pyovpn-2.0-py2.7.egg
Let’s find the files that have been modified.
$
Files ./pyovpn-2.0-py2.7_hacked/pyovpn/lic/uprop.pyo and ./pyovpn-2.0-py2.7_original/pyovpn/lic/uprop.pyo differ
Only in ./pyovpn-2.0-py2.7_hacked/pyovpn/lic: uprop2.pyo
Files ./pyovpn-2.0-py2.7_hacked/pyovpn/production.pyo and ./pyovpn-2.0-py2.7_original/pyovpn/production.pyo differ
Now let’s compare the files themselves, but first we need to decompile them, since .pyc is bytecode. We’ll use the decompile6 utility, which works great with Python 2.7, 3.7, and 3.8. I’ll be doing everything on macOS, but the commands are unlikely to differ on Linux.
$ pip install decompyle6
$ uncompyle6 /Users/n0a/Work/openvpn_decompile/test_diff/pyovpn-2.0-py2.7_hacked/pyovpn/lic/uprop.pyo > uprop.py
$ cat uprop.py
Let’s look at the contents of the decompiled file:
$ cat uprop.py
import uprop2old_figure = Nonedef new_figure(self, licdict): ret = old_figure(self, licdict) ret['concurrent_connections'] = 1024 return retfor x in dir(uprop2): if x[:2] == '__': continue if x == 'UsageProperties': exec 'old_figure = uprop2.UsageProperties.figure' exec 'uprop2.UsageProperties.figure = new_figure' exec '%s = uprop2.%s' % (x, x)Interesting! The for loop iterates over all attributes of uprop2, skipping any whose names start with two underscores. The old_figure function is turned into a reference to the figure method of the UsageProperties class, and the function in the UsageFigure class is redirected to new_figure. It’s hard to say why this was done. My guess is that UsageProperties is used elsewhere, and to avoid changing it everywhere, they pulled this not-so-obvious trick.
We decompile uprop2 and realize it’s actually the original uprop, where the license check is performed.
...class UsageProperties(object): def figure(self, licdict): proplist = set(('concurrent_connections',)) good = set() ret = None if licdict: for key, props in licdict.items(): if 'quota_properties' not in props: print 'License Manager: key %s is missing usage properties' % key continue proplist.update(props['quota_properties'].split(',')) good.add(key) for prop in proplist: v_agg = 0 v_nonagg = 0 if licdict: for key, props in licdict.items(): if key in good: if prop in props: try: nonagg = int(props[prop]) except: raise Passthru('license property %s (%s)' % (prop, props.get(prop).__repr__())) v_nonagg = max(v_nonagg, nonagg) prop_agg = '%s_aggregated' % prop agg = 0 if prop_agg in props: try: agg = int(props[prop_agg]) except: raise Passthru('aggregated license property %s (%s)' % (prop_agg, props.get(prop_agg).__repr__())) v_agg += agg if DEBUG: print 'PROP=%s KEY=%s agg=%d(%d) nonagg=%d(%d)' % (prop, key, agg, v_agg, nonagg, v_nonagg) apc = self._apc() v_agg += apc if ret == None: ret = {} ret[prop] = max(v_agg + v_nonagg, bool('v_agg') + bool('v_nonagg')) ret['apc'] = bool(apc) if DEBUG: print "ret['%s'] = v_agg(%d) + v_nonagg(%d)" % (prop, v_agg, v_nonagg) return retThe crack author used object substitution without modifying the main executable. The approach is clear.
Now let’s try the same thing with the current version of OpenVPN Access Server. I’ll be testing on a VPS running Debian 11 (Bullseye).
Download the latest version from the developer’s site. Install it or extract the .deb archive, then check which Python version pyovpn targets:
$ ls /usr/local/openvpn_as/lib/python/pyovpn-2.0-py3.9.egg
Right, Python 3.9, which isn’t ideal, since decompilation for that version isn’t supported yet.
info
You can support the decompiler’s developer and help move the project forward. Learn more in his sponsorship message: sponsorship message.
I have a hunch the Debian 10 build is the right choice, because from the wiki we learn that Debian 11 (Bullseye) ships Python 3.9, while Debian 10 (Buster) has 3.7.
Installing on Debian 10
Since the VPS is new, I’ll just switch the OS to version 10 and see which OpenVPS-AS versions are available:
$
wget -qO – https://as-repository.openvpn.net/as-repo-public.gpg | apt-key add –
echo “deb http://as-repository.openvpn.net/as/debian buster main”>/etc/apt/sources.list.d/openvpn-as-repo.list$ $
openvpn-as:
Installed: (none)
Candidate: 2.10.1-d5bffc76-Debian10
Version table:
2.10.1-d5bffc76-Debian10 500
500 http://as-repository.openvpn.net/as/debian buster/main amd64 Packages
2.10.0-ca1e86b5-Debian10 500
500 http://as-repository.openvpn.net/as/debian buster/main amd64 Packages
2.9.6-1090f6b3-Debian10 500
500 http://as-repository.openvpn.net/as/debian buster/main amd64 Packages
2.9.5-82d54e5b-Debian10 500
500 http://as-repository.openvpn.net/as/debian buster/main amd64 Packages
2.9.4-8b3ce898-Debian10 500
…
Great—the version is the latest, same as in v11, so it’s up to date. Install it and check which Python version pyovpn uses. It should be 3.7.
$ apt -y install openvpn-as
$ ls /usr/local/openvpn_as/lib/python/pyovpn-2.0-py3.7.egg
/usr/local/openvpn_as/lib/python/pyovpn-2.0-py3.7.egg
Exactly: this is the latest version (2.10.1) and it uses Python 3.7. That lines up. Let’s check how much has changed compared to version 2.0.5, which was initially found to be hacked. To avoid shuffling files back and forth, I’m installing python-decompile3 on the server:
$ git clone https://github.com/rocky/python-decompile3
$ cd python-decompile3
$ pip3 install -e .Once again, extract the .egg:
$ $ $ $ $
EGG-INFO pyovpn
Next, decompile it:
$ cd ./pyovpn/lic
$ decompyle3 uprop.pyc > uprop.py
$ cat uprop.py
Looking at the differences, everything is the same except for minor syntax discrepancies. But what was the author of the hack aiming for by using two files? Why not just explicitly specify the number of connections at the end of the figure function, bypassing all the checks?
Let’s try a simpler approach. Add ret[ right before returning (ret):
$ nano uprop.py
... apc = self._apc() v_agg += apc if ret == None: ret = {} ret[prop] = max(v_agg + v_nonagg, bool('v_agg') + bool('v_nonagg')) ret['apc'] = bool(apc) if DEBUG: print("ret['%s'] = v_agg(%d) + v_nonagg(%d)" % (prop, v_agg, v_nonagg)) ret['concurrent_connections'] = 1337 return ret def _apc(self):...Save the file and compile:
$ python3 -m compileall uprop.py
$ rm uprop.pyc uprop.py
$ cp __pycache__/uprop.cpython-37.pyc ./uprop.pyc
$ rm -Rf __pycache__
Archive and replace the .egg package:
$ cd /opt/ovpn
$ zip -r *$ sudo rm /usr/local/openvpn_as/lib/python/pyovpn-2.0-py3.7.egg
$ sudo cp common.zip /usr/local/openvpn_as/lib/python/pyovpn-2.0-py3.7.egg
And restart OpenVPN-AS. I cleared the logs because I was encountering errors while experimenting. That step is optional.
$ sudo service openvpnas stop
$ sudo rm /var/log/openvpnas.log
$ sudo touch /var/log/openvpnas.log
Start the openvpnas service:
$ service openvpnas start
Verify that everything is OK and there are no errors:
cat /var/log/openvpnas.log
Head to the admin panel at :943/admin and you’ll see that 1,337 connections are available.

info
If you’ve lost your password and can’t access the admin panel, run passwd .
Testing showed excellent performance with two or more devices.
Conclusions
With many VPN services currently blocked or unable to accept payments, Access Server is a solid way to quickly spin up your own alternative with user profile management. Whether you use a pirated activation method is up to you—but as it turns out, it’s not hard at all.