比赛的时候对很多东西不熟悉,导致没做出来,赛后复现一下,理清楚流程后会做的很顺利
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按钮,可以看到服务端有正确的响应
把apk后缀改成zip解压,有个classes.dex,用dex2jar把他转成jar
d2j-dex2jar.bat classes.dex
会报个错,不用理会,用jd-gui打开jar文件
函数比较少,前面我们点set按钮后有个post请求,应该就是调用了post这个函数,再搜索一下哪个地方调用了post函数,可以找到runnable这里
post接收两个参数,第一个是根据我们的输入拼接的url:http://192.168.41.8:5000/,第二个是一个将一个JSON对象转换成字符串后再用私钥加密的字符串,JSON对象里存了IMEI,手机型号等信息
_private_encrypto_接收一个字符串参数,将其加密,私钥为Integer.valueOf(1)
将密钥提取出来,补上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 | from flask import Flask,request |
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
14def 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 | class Phone(object): |
访问http://192.168.41.8:5000/的时候会执行 hello
1 |
|
获得从apk发送的 request.data 并解密,转成字典,用IMEI正则匹配是否是长度为15的纯数字字符串,是的话创建一个phone对象,将其序列化写入phone目录下,文件名是IMEI的值
访问192.168.41.8:5000/search的时候会执行 search
1 |
|
从表单获取加密过的imei并解密,正则匹配,是15个数字的话加载相应的文件将其反序列化
访问192.168.41.8:5000/upload的时候会执行 upload
1 |
|
获取上传的文件保存到image目录下,注意到upload没有正则匹配
访问192.168.41.8:5000/check的时候会执行 check
1 |
|
从表单获取文件名,正则匹配,是15个数字的话在返回文件内容
这几个函数都不复杂,由于 upload 获取文件名时没有正则匹配,可以输入带有 . 和 / 的文件名,意味着可以在任意路径下写
而search函数可以把phone目录下的文件反序列化,那么我们可以构造一个序列化文件,将其上传到phone目录下,再调用search就可以反序列化,执行我们的代码
0x03 构建exp
流程: 创建序列化文件 --> upload --> search --> 任意代码执行
创建序列化文件
构建一个类,添加一个 __reduce__() 魔术方法
1 | import cPickle |
漏洞产生的原因在于其可以将自定义的类进行序列化和反序列化,反序列化后产生的对象会在结束时触发__reduce__()函数从而触发恶意代码,序列化文件名只要是15个数字即可
这里为了方便,直接将读取的flag写入image文件夹下,最后可以用 check 函数读出来,也有其他方法获取flag
上传到服务端
注意设置的文件名要在前面加 ../phone/,将其写入phone目录下
1 | import requests |
加密文件名
由于search 接受的是一个经过私钥加密的字符串,所以我们要先将IMEI加密,再post
1 | import M2Crypto |
这一步可以修改下index.py里的_publicdecrypt函数输出 msg 和解密后的 output,验证下这个函数加密出来的内容和apk加密的是否一致
反序列化 & 输出flag
触发search函数,反序列化执行代码,再用check函数读出flag
1 | def deserialization(): |
完整exp
1 | import cPickle |
0x04 Reference
0x05 附件
题目压缩包: https://pan.baidu.com/s/1iBdJigY46Rv0r22pWm15qA 提取码: hmah