皮蛋厂的学习日记系列为山东警察学院网安社成员日常学习分享,希望能与大家共同学习、共同进步~
-
2020级 sp4c1ous | JAVA反序列化之反序列化基础
-
前言
-
前置知识
-
小结
-
2020级 AndyNoel | Wordpress漏洞挖掘分析
-
WordPress简析
-
WordPress 插件
-
插件如何嵌入WordPress主程序
-
插件漏洞测试与挖掘
-
安全性
WEB
2020级 sp4c1ous | JAVA反序列化之反序列化基础
前言
在之前我们已经学习过了 反射 和 RMI 的一些相关内容,并且在 RMI 的结尾留了一个关于 classAnnotations 的小尾巴,因为反序列化所需要的知识不足没有进行更深入的分析。
在这一篇文章中,我们会像学习 RMI 那样深入地学习 反序列化 的相关知识
前置知识
序列化与反序列化的基础概念
这一个部分感觉也不太需要多加赘述,在此之前大家应该都已经了解过 PHP 的反序列化了,两者在基础的概念上是基本上一致的。
记得在曾经学习 PHP 反序列化的时候,是通过 JSON 来类比学习的序列化与反序列化的概念。
这是当时的笔记,同时 JAVA 反序列化和 PHP 的反序列化是很像的,他们都是只能将对象中的属性按照格式生成一段数据流,然后在反序列化的时候将属性再赋值给新的对象,不过 JAVA 提供了更加高级、灵活的方法 writeObject ,允许开发者在序列化流中插入一些自定义数据,进而在反序列化的时候能够使用 readObject 进行读取。
序列化与反序列化的概念本身就不难懂,这里我们将目光直接放到 JAVA 语言中的反序列化上。
一门成熟的语言在往上传递信息的时候通常会用到一些标准的格式化数据,比如 JSON
、XML
,这种东西叫通用数据交互格式,通常用于不同语言、不同环境下的数据交换,比如前端的 JS 就会通过 JSON 和后端服务进行通信、微信服务器会通过 XML 和公众号服务器进行通信,但是这两个数据格式都有一个共同的问题,不支持复杂的数据类型。他们只能传输基本的数据类型,整型、浮点型、字符串、布尔型等,如果开发者想要在传输数据的时候直接传输一个对象,那么就要想办法扩展基础的 JSON 或者 XML 语法。
比如,jackson、fastjson 这类序列化库,这些库在上面两种通用数据交互格式的基础上进行了改造,通过特定的语法来传递对象,或者也有像 RMI 这样,直接荣光 JAVA 语言内置的序列化放啊,直接将对象转化为一串二进制数,也就是我们常见的 0xaced…
但是不管是什么序列化与反序列化方法,只要牵扯到了这种数据传输的形式,就很有可能导致安全问题的发生。不过根据之前语言中的“反序列化漏洞”的概念我们也可以知道,这是一种综合性非常强的漏洞,可能会涉及到很多其他的漏洞,同时还需要非常强的代码审计功底与漏洞挖掘能力等。
在 JAVA 中还需要注意一点,不同反序列化方法导致的漏洞之间的差别是完全不同的,比如 Jackson 反序列化漏洞和 Java readObject 造成的反序列化漏洞就是完全不同的两种漏洞。
JAVA 反序列化基础
这里首先对着写了一个 JAVA 反序列化的 demo
package serialize.test;
import java.io.*;
interface Cars {
public void drive();
}
class Car implements Serializable {
public String brand;
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
//执行默认的readObject()方法
in.defaultReadObject();
//执行打开计算器程序命令
Runtime.getRuntime().exec("calc");
}
}
class Benz extends Car implements Cars {
@Override
public void drive() {
System.out.println("wuwuwuwuwu.");
}
}
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Car benz = new Benz();
benz.brand = "benz";
FileOutputStream fos = new FileOutputStream("obj");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(benz);
os.close();
//从文件中反序列化obj对象
FileInputStream fis = new FileInputStream("obj");
ObjectInputStream ois = new ObjectInputStream(fis);
//恢复对象
Benz objectFromDisk = (Benz) ois.readObject();
System.out.println(objectFromDisk.brand);
ois.close();
}
}
首先我们可以看到,这里引入了 import java.io.*;
,这是由于 JAVA 本身的反序列化是要通过继承 java.io.Serializable
接口来实现的。
上面的 demo 中,我们创建了一个Car
父类和一个Benz
子类,以及一个Cars
接口,我们可以通过在子类中重写接口的drive
方法来实现卡车的功能。
我们先把重点放在 Test 类中
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Car benz = new Benz();
benz.brand = "benz";
FileOutputStream fos = new FileOutputStream("obj");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(benz);
os.close();
//从文件中反序列化obj对象
FileInputStream fis = new FileInputStream("obj");
ObjectInputStream ois = new ObjectInputStream(fis);
//恢复对象
Benz objectFromDisk = (Benz) ois.readObject();
System.out.println(objectFromDisk.brand);
ois.close();
}
}
在这里有我们的 main 方法,我们在里面创建了一个 Benz 对象,通过文件流的形式来 将对象写入到磁盘中实现持久性存储 ,这个写入的过程就是序列化的过程。我们可以详细看一下这个过程:
首先,类 ObjectInputStream 和 ObjectOutputStream 是高层次的数据流,它们包含反序列化和序列化对象的方法。
ObjectOutputStream 类包含很多写方法来写各种数据类型,但是一个特别的方法例外:
public final void writeObject(Object x) throws IOException
与之相对的还有 ObjectInputStream 类的这个方法:
public final Object readObject() throws IOException,
ClassNotFoundException
他们的作用分别是 序列化一个对象,并将它发送到输出流 以及 从流中取出下一个对象,并将对象反序列化
我们在序列化的过程中,首先实例化了 FileOutputStream 类,new FileOutputStream("obj");
,用这个对象来将 Benz 的对象存储到 obj ,然后实例化 ObjectOutputStream 类,调用 writeObject
方法实现序列化,反序列化同上,也是一顿实例化与调用,最后利用 readObject
实现反序列化。
我们要研究的是反序列化漏洞,那我们就要将目光集中于 readObject
这个方法上。
在最开始的理解中,我们可以将readObject()
=__wakeup()
,writeObject()
=__sleep()
这两个方法是默认可以不写的,但是也可以通过自定义的writeObject()
和readObject()
方法控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。
不过其实不全对,虽然都是在反序列化的时候触发,但他们解决的问题稍微有些差异 Java 设计 readObject
的思路和PHP的 __wakeup
不同点在于:readObject
倾向于解决“ 反序列化时如何还原一个完整对象 ”这个问题,而PHP的 __wakeup
更倾向于解决“ 反序列化后如何初始化这个对象 ”的问题。
readObject() 与 __wakeup() 之差
其实也就是 JAVA 与 PHP 之差,我们先来看 PHP
PHP
首先我们来尽量清晰地认识一下 __wakeup()
这个 PHP 中的魔术方法。
PHP 的序列化与反序列化的过程是开发者不能参与的,是一个经过调用函数(serialize
unserialize
)之后就完成了的过程,我们不能在序列化的时候还能往序列化的数据流中新增一个内容,我们只能通过在序列化函数执行之前,通过对属性的更改来改变序列化的内容,或者对序列化出来的字符串进行更改然后去影响反序列化。
而我们刚刚拿来类比的 __sleep
、__wakeup
以及其他魔术方法也只是写在代码中,在序列化、反序列化的前后进行一些操作而已。PHP 的序列化、反序列化是一个 纯内部 的过程。
P牛这里举例了数据库的链接,确实能很清晰地呈现上面的观点:
<?php
class Connection
{
protected $link;
private $dsn, $username, $password;
public function __construct($dsn, $username, $password)
{
$this->dsn = $dsn;
$this->username = $username;
$this->password = $password;
$this->connect();
}
private function connect()
{
$this->link = new PDO($this->dsn, $this->username, $this->password);
}
}
这里就是一个数据库链接的类,其中有一个 connect
方法,但是在 PHP 中,有一个概念叫资源(资源是一种特殊的变量类型,保存了到外部资源的一个引用:如打开文件、数据库连接、图形画布区域等),资源类型的对象默认是不会写入序列化数据中的,所以我们上面的 $link
属性实际上还是 null 在反序列化后也会是 null 。
如果我们希望那道反序列化的时候拿到的就是一个数据库连接,那么我们就要编写 __wakeup
方法
<?php
class Connection
{
protected $link;
private $dsn, $username, $password;
public function __construct($dsn, $username, $password)
{
$this->dsn = $dsn;
$this->username = $username;
$this->password = $password;
$this->connect();
}
private function connect()
{
$this->link = new PDO($this->dsn, $this->username, $this->password);
}
public function __sleep()
{
return array('dsn', 'username', 'password');
}
public function __wakeup()
{
$this->connect();
}
}
我们增加了 __wakeup 魔术方法, __wakeup
魔术方法可以在反序列化那道 Connection 对象后,执行connect()
函数,$this->connect();
,这样就实现了反序列化的时候拿到的就是一个数据库连接。
__wakeup
的作用在反序列化后,执行一些初始化操作。但其实我们很少利用序列化数据传递资源类型的对象,而其他类型的对象,在反序列化的时候就已经赋予其值了。-
所以你会发现,PHP的反序列化漏洞,很少是由 __wakeup
这个方法触发的,通常触发在析构函数 __destruct
里。其实几乎所有的PHP反序列化漏洞,都并不是由反序列化导致的,只是通过反序列化可以控制对象的属性,进而在后续的代码中进行危险操作。
JAVA
在 JAVA 的反序列化中,很多是需要开发者深入参与的,所以大量地库都会去实现 readObject
和 writeObject
方法,这和上面 PHP 中的 __wakeup
与 __sleep
很少出现是形成了鲜明对比的
JAVA 在序列化一个对象时,会调用这个对象中的 writeObject 方法,参数类型是 ObjectOutputStream ,开发者可以将任何内容写入这个 stream 中;反序列化时会调用 readObject ,开发者也可以从中读取出前面写入的内容,并进行处理。
package serialize.test;
import java.io.*;
interface Cars {
public void drive();
}
class Car implements Serializable {
public String brand;
private void readObject(java.io.ObjectInputStream s) throws IOException,ClassNotFoundException {
s.defaultReadObject();
String message = (String) s.readObject();
System.out.println(message);
}
private void writeObject(java.io.ObjectOutputStream s) throws IOException {
s.defaultWriteObject();
s.writeObject("This is a object"); //可见,这里在执行完默认的 s.defaultWriteObject() 后,向stream里写入了一个字符串 This is a object 。
}
}
class Benz extends Car implements Cars {
@Override
public void drive() {
System.out.println("wuwuwuwuwu.");
}
}
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Car benz = new Benz();
benz.brand = "benz";
FileOutputStream fos = new FileOutputStream("D:\object.txt");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(benz);
os.close();
//从文件中反序列化obj对象
FileInputStream fis = new FileInputStream("D:\object.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
//恢复对象
Benz objectFromDisk = (Benz) ois.readObject();
System.out.println(objectFromDisk.brand);
ois.close();
}
}
执行
写到个文件里,然后winhex打开复制一下hex
ACED00057372001373657269616C697A652E746573742E42656E7A968EC05F349B4DAD0200007872001273657269616C697A652E746573742E43617262CB32AE584A39430300014C00056272616E647400124C6A6176612F6C616E672F537472696E673B787074000462656E7A740010546869732069732061206F626A65637478
再用 SerializationDumper
反序列化一下
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 19 - 0x00 13
Value - serialize.test.Benz - 0x73657269616c697a652e746573742e42656e7a
serialVersionUID - 0x96 8e c0 5f 34 9b 4d ad
newHandle 0x00 7e 00 00
classDescFlags - 0x02 - SC_SERIALIZABLE
fieldCount - 0 - 0x00 00
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_CLASSDESC - 0x72
className
Length - 18 - 0x00 12
Value - serialize.test.Car - 0x73657269616c697a652e746573742e436172
serialVersionUID - 0x62 cb 32 ae 58 4a 39 43
newHandle 0x00 7e 00 01
classDescFlags - 0x03 - SC_WRITE_METHOD | SC_SERIALIZABLE
fieldCount - 1 - 0x00 01
Fields
0:
Object - L - 0x4c
fieldName
Length - 5 - 0x00 05
Value - brand - 0x6272616e64
className1
TC_STRING - 0x74
newHandle 0x00 7e 00 02
Length - 18 - 0x00 12
Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 03
classdata
serialize.test.Car
values
brand
(object)
TC_STRING - 0x74
newHandle 0x00 7e 00 04
Length - 4 - 0x00 04
Value - benz - 0x62656e7a
objectAnnotation
TC_STRING - 0x74
newHandle 0x00 7e 00 05
Length - 16 - 0x00 10
Value - This is a object - 0x546869732069732061206f626a656374
TC_ENDBLOCKDATA - 0x78
serialize.test.Benz
values
可见,我们写入的字符串 This is a object 被放在 objectAnnotation 的位置
这个形式我们眼熟,我们上一篇文章 RMI 基础就是在一个叫 classAnnotations 的东西的位置截止的,虽然 P牛当时就进行了分析,但当时我没有接触反序列化的基础知识看得有一些云里雾里,现在我们来具体地分析一下这两种东西。
首先,我们知道,在 JAVA 序列化地时候用到了一个类 ObjectOutputStream
。在这个类的内部有一个方法,叫做 annotateClass
❝
子类可以实现此方法以允许将类数据存储在流中。默认情况下,此方法不执行任何操作。ObjectInputStream 中对应的方法是resolveClass。对于流中的每个唯一类,该方法只调用一次。类名和签名将已写入流。此方法可以免费使用 ObjectOutputStream 来保存它认为合适的类的任何表示(例如,类文件的字节)。ObjectInputStream 对应子类中的resolveClass 方法必须读取和使用annotateClass 写入的任何数据或对象。
❝
简单来说 ObjectOutputStream 的子类有需要向序列化后的数据里放任何内容,都可以重写这个方法,写入你自己想要写入的数据。然后反序列化时,就可以读取到这个信息并使用。
在 RMI 规范中,对于每个写入 RMI 编组流的类描述符,annotateClass 方法将把类对象调 java.rmi.server.RMIClassLoader.getClassAnnotation 的结果添加到流中,简单来说就是我们RMI的类 MarshalOutputStream 会将当前的 codebase 写入
/rmi/server/LoaderHandler.java
/rmi/server/RMIClassLoader.java
这样就形成了我们在反序列化 RMI 传输过程中的序列化时看到的景象,我们在分析序列化数据时看到的 classAnnotations ,实际上就是 annotateClass 方法写入的内容。
objectAnnotation 就更简单了,就是 ObjectOutputStream 类的 annotateClass 方法往流里写入的呗。
小结
到这里基础性的东西就算是总结得差不多了,接下来我们的学习就要开始往各种攻击手法上靠了。
下一章的事情下一章再说拉。
2020级 AndyNoel | Wordpress漏洞挖掘分析
WordPress简析
WordPress是目前使用最广泛的CMS,65.2%的使用CMS的站点采用WordPress,WordPress使用插件和主题实现个性化和各种功能。
WordPress 插件
通过插件目录 /wp-contents/plugins/ 中某个文件的存在如下meta信息识别一个插件。
/**
* Plugin Name: My Basics Plugin
* Plugin URI: https://example.com/plugins/the-mbasics/
* Description: Handle the basics with this plugin.
* Version: 1.10.3
* Requires at least: 5.2
* Requires PHP: 7.2
* Author: John Smith
* Author URI: https://author.example.com/
* License: GPL v2 or later
* License URI: https://www.gnu.org/licenses/gpl2.0.html
* Update URI: https://example.com/my-plugin/
* Text Domain: my-basics-plugin
* Domain Path: /languages
*/
插件如何嵌入WordPress主程序
代码直接执行
WordPress内核会在加载插件时将包含meta数据的插件文件包含,此时可以直接执行php代码影响WordPress运行。
❝
They provide a way for running a function at a specific point in the execution of WordPress Core, plugins, and themes
Hooks
Actions
在WordPress内核和插件主题的特定点位执行一些函数,这些函数通常没有返回值。
// 关键函数
// add_action( string $hook_name, callable $callback, int
$priority = 10, int $accepted_args = 1 );
add_action( 'save_post', 'wpdocs_my_save_post', 10, 3 );
// do_action( string $hook_name, mixed $arg )
do_action( 'save_post', $post_ID, $post, $update );
function wpdocs_my_save_post( $post_ID, $post, $update ) {
// do stuff here
}
Filters
❝
They provide a way for functions to modify data during the execution of WordPress Core, plugins, and themes.
filters主要是对特定数据进行修改过滤,这些函数有返回值。
// 关键函数
// add_filter( string $hook_name, callable $callback, int
$priority = 10, int $accepted_args = 1 )
add_filter( 'the_title', 'wporg_filter_title' );
apply_filter( 'the_title', $title );
function wporg_filter_title( $title ) {
return 'The ' . $title . ' was filtered';
}
admin menu
插件在后台添加菜单
// 顶级菜单
add_menu_page( $page_title, $menu_title, $capability,
$menu_slug, $function, $icon_url, $position );
// $function回调直接打印菜单内容
// 子菜单
add_submenu_page( $parent_slug, $page_title, $menu_title,
$capability, $menu_slug, $function );
//1、在仪表盘添加子菜单: add_submenu_page( 'index.php', … );
//2、在文章处添加子菜单: add_submenu_page( 'edit.php', … );
//3、在媒体处添加子菜单: add_submenu_page( 'upload.php', … );
//4、在链接处添加子菜单: add_submenu_page( 'link-manager.php',… );
//5、在页面处添加子菜单: add_submenu_page( 'edit.php?post_type=page', … );
//6、在评论处添加子菜单: add_submenu_page( 'editcomments.php', … );
//7、在你自定义文章类型处添加子菜单: add_submenu_page('edit.php?post_type=your_post_type',…)
//8、在外观处添加子菜单: add_submenu_page( 'themes.php', … );
//9、在插件处添加子菜单: add_submenu_page( 'plugins.php', … );
//10、在用户处添加子菜单: add_submenu_page( 'users.php', … );
//11、在工具处添加子菜单: add_submenu_page( 'tools.php', … );
//12、在设置处添加子菜单: add_submenu_page( 'optionsgeneral.php', … )
或者直接用封装好的。
short codes
❝
Shortcodes are macros that can be used to perform dynamic interactions with the content. i.e creating a gallery from images attached to the post or rendering a video.
短代码宏可以在文章中动态的插入内容。
add_shortcode(
string $tag,
callable $func
);
add_shortcode('wporg', 'wporg_shortcode');
// [wporg]
function wporg_shortcode( $atts = [], $content = null) {
// do something to $content
// always return
return $content;
}
// Enclosing Shortcodes
// [wporg title="WordPress.org" ]content to
manipulate[/wporg]
function wporg_shortcode( $atts = array(), $content = null
) {
// do something to $content
// always return
return $content;
}
add_shortcode( 'wporg', 'wporg_shortcode' )
插件漏洞测试与挖掘
入口
上面介绍了常用的插件影响WordPress运行的方法,也就对应了相关函数中存在漏洞触发的方法。下面还有一些常用的
ajax
WordPress支持ajax进行后台操作,插件可以通过action hook。WordPress内置了很多ajax相关的hook
其中自定义hook很常用,也容易出现漏洞。
// wp_ajax_$action
add_action( 'wp_ajax_foobar', 'my_ajax_foobar_handler' );
function my_ajax_foobar_handler() {
// Make your response and echo it.
// Don't forget to stop execution afterward.
wp_die();
}
// wp_ajax_nopriv_$action
// 这个hook是无权限用户可以ajax访问的hook
而这个hook的触发方式就是向 /wp-admin/admin-ajax.php POST, 其中action 参数值为 wp_ajax_action 内容。
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: 192.168.223.130
Content-Type: application/x-www-form-urlencoded;
charset=UTF-8
X-Requested-With: XMLHttpRequest
Cookie: [cookie]
action=$action&data=data
sink
对于sql查询WordPress封装了 wpdb
类,暴露出一个全局变量$wpdb
,也提供了快速修改WordPress存储在数据库中设置的API比如
add_options/update_options/get_options
。
使用 $wpdb->query($sql)
; 查询语句时也可能存在sql注入。
source
WordPress貌似没有特别封装source输入。不过add_query_arg/remove_query_arg
在 $url
参数未指定时,返回值会
带有 $_SERVER['REQUEST_URI']
, 那我们也可以看作一个source。
安全性
sanitization
首先WordPress存在全局过滤wp-includes/load.php
// wordpress 中常用的过滤函数
// 单双引号
esc_sql
// 单双引号&html_entity
esc_attr、esc_html、esc_js、esc_html__、esc_attr__、
esc_textarea、esc_url_raw
https://developer.wordpress.org/plugins/security/securingoutput/
// 完全过滤
absint、esc_url、sanitize_key
// 其他的详细过滤
https://developer.wordpress.org/plugins/security/securinginput/
role & capabilities
WordPress 默认定义了以下的角色
-
Super Admin – somebody with access to the site network administration features and all other features. See the Create a Network article. -
Administrator ( slug: ‘administrator’ ) – somebody who has access to all the administration features within a single site. -
Editor ( slug: ‘editor’ ) – somebody who can publish and manage posts including the posts of other users. -
Author ( slug: ‘author’ ) – somebody who can publish and manage their own posts. -
Contributor ( slug: ‘contributor’ ) – somebody who can write and manage their own posts but cannot publish them. -
Subscriber ( slug: ‘subscriber’ ) – somebody who can only manage their profile.
在这里可以看到对应的role有对应的权限。在代码中可能会先检查权限。
/ current_user_can( $capability );
current_user_can( 'edit_posts' );
user_can( $user, $capability );
nonce
wp_create_nonce()
和wp_verify_nonce()
对于 nonce
在表单中使用,有一个助手函数(helper function)叫wp_nonce_field()
,可以用来创建一个 nonce
隐藏字段。下面这个表单可以和上面例子配合:
<form id="weibo-form">
<label for="weibo-name">
<?php esc_html_e( 'Weibo', 'text-domain' ); ?>
</label>
<input type="text" id="weibo-name" value="<?php echo esc_attr( get_option( 'weibo', '') ); ?>" />
<?php
wp_nonce_field( 'weibo-save-nonce' );
submit_button( __( 'Save', 'text-domain' ) );
?>
</form>
<script>
jQuery( document ).ready(function($){
$( '#weibo-form' ).on( 'click', function(e){
$.post( ajaxurl, {
'weibo-save-nonce' : $( '#_wpnonce'
).val()
weibo: $( '#weibo-name')
})
});
});
</script>
nonce可以作为csrf token,也可以验证用户权限,因为特定nonce只有特定用户(比如admin)页面才会生成。
其中 wp_verify_nonce
验证nonce
时只会返回True/False
,而
check_ajax_referer/check_admin_referer
验证失败会直接die
原文始发于微信公众号(山警网络空间安全实验室):皮蛋厂的学习日记 | 2022.03.09 JAVA反序列化之反序列化基础 & Wordpress漏洞挖掘分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论